diff --git a/CLAUDE.md b/CLAUDE.md index ec4027a9..ee3e33d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -131,5 +131,5 @@ headings, Inter for body — matching `web-app/src/theme.ts`. (`cb6eab9`, 2026-04-18). Rotation is operator-gated. Playbook: parent `docs/SECURITY_INCIDENTS.md`. -See `docs/TODO.md` for the integration backlog and `ROADMAP_AUTH_2026-05-30.md` (parent) for +See the open GitHub issues for the integration backlog and `ROADMAP_AUTH_2026-05-30.md` (parent) for the auth program tracker. diff --git a/README.md b/README.md index 0c7ba439..6516f752 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,10 @@ Google Authenticator or Microsoft Authenticator: tap-to-copy, swipe-delete confirmation, manual account entry The core engine lives in `shared/src/commonMain/kotlin/com/fivucsas/authenticator/totp/` -via KMP `expect`/`actual`, so iOS and Desktop will reuse it once their HMAC -actuals land (tracked in `docs/TODO.md`). QR code scanning is deferred; manual -entry is the supported flow in 5.1.0. +via KMP `expect`/`actual`; the Android, Desktop and iOS HMAC actuals are all +implemented, so the engine is shared across platforms. QR-code account import +is supported on Android (`OtpQrScannerScreen` + `OtpauthUri.parse()`) alongside +manual entry. ## Feature coverage matrix diff --git a/ROADMAP_CLIENT_APPS.md b/ROADMAP_CLIENT_APPS.md deleted file mode 100644 index 7708b734..00000000 --- a/ROADMAP_CLIENT_APPS.md +++ /dev/null @@ -1,125 +0,0 @@ -# FIVUCSAS Client-Apps & Web-App Completion Roadmap - -> **SUPERSEDED** — The hosted-first pivot (2026-04-16) replaced the 14-feature parity matrix below. -> Native clients are now thin OAuth wrappers. See [docs/TODO.md](docs/TODO.md) for current priorities. - -**Created:** 2026-04-04 -**Last Updated:** 2026-04-05 -**Current Completion:** 100% (Phase 1-3 complete) -**APK Release:** v2.0.0 (https://github.com/Rollingcat-Software/client-apps/releases/tag/v2.0.0) - ---- - -## Phase 1: Critical Build Fixes — COMPLETE - -All 8 items done on 2026-04-04. - ---- - -## Phase 2: Core Feature Completion — 8/8 DONE - -| # | Task | Status | -|---|------|--------| -| 2.1 | Multi-step auth flow UI | DONE | -| 2.2 | Verification pipeline | DONE | -| 2.3 | Voice search UI | DONE | -| 2.4 | Step-up authentication | DONE | -| 2.5 | Role/permission management | DONE | -| 2.6 | Audit log admin dashboard | DONE | -| 2.7 | iOS real implementations (AVFoundation, Keychain, LocalAuthentication, WebAuthn passkeys) | DONE | -| 2.8 | Desktop WebAuthn (software ECDSA authenticator + credential store) + Fingerprint (PKCS12 keystore) | DONE | - ---- - -## Phase 3: Polish & Production — 7/7 DONE - -| # | Task | Status | -|---|------|--------| -| 3.1 | Developer portal screen | DONE | -| 3.2 | Widget demo screen | DONE | -| 3.3 | Test coverage expansion (277 tests, 21 ViewModel test classes) | DONE | -| 3.4 | CSV export for analytics/audit (CsvExporter + platform file savers) | DONE | -| 3.5 | E2E testing framework (3 Compose UI test files, 17 instrumented tests) | DONE | -| 3.6 | Gradle 10 compatibility (deprecated APIs cleaned, Gradle 9.4.1) | DONE | -| 3.7 | Release signing keystore | DONE | - ---- - -## Phase 4: Maintenance - -| # | Task | Status | -|---|------|--------| -| 4.1 | Resource consolidation | TODO | -| 4.2 | Client-side ML migration (TFLite voice, on-device YOLO) | TODO | -| 4.3 | Istanbul card YOLO training data | TODO | - ---- - -## Web-App Feedback Backlog (Chrome Mobile Testing 2026-04-04) - -| # | Item | Status | -|---|------|--------| -| W1 | Face enrollment slow on mobile (MediaPipe fails, timeout fallback) | KNOWN LIMITATION | -| W2 | Face search (matches vs results field mismatch) | FIXED | -| W3 | NFC auth token (wrong localStorage key → DI TokenService) | FIXED | -| W4 | NFC "Whose Card?" for all users | FIXED | -| W5 | Card detection frozenset crash | FIXED | -| W6 | Istanbul card misclassified as ehliyet | KNOWN LIMITATION | -| W7 | Forgot password flow (code + new password) | FIXED | -| W8 | Phone number dialog for SMS OTP enrollment | FIXED | -| W9 | Non-admin personal dashboard | FIXED | -| W10 | Login page: removed broken alt auth, added MFA info text | FIXED | -| W11 | Notifications: user activity via /my/activity endpoint | FIXED | -| W12 | User activity/summary page | PARTIALLY DONE (dashboard has it) | -| W13 | OAuth widget demo tenant (Marmara Exam Portal created) | DONE | -| W14 | GitHub repos organization/cleanup (all 8 repos tagged) | DONE | -| W15 | User audit log view (via /my/activity endpoint) | FIXED | -| W16 | Cross-device session awareness (SessionsSection in Settings, i18n EN+TR) | DONE | -| W17 | Voice STT verification (speech-to-text matching) | FUTURE | -| W18 | BYOD (Bring Your Own Database) | FUTURE | -| W19 | BaaS per-feature API rental model | FUTURE | - -### Additional Issues Found (Late Session 2026-04-04) - -| # | Item | Priority | Status | -|---|------|----------|--------| -| W20 | Biometric Tools page width overflow on mobile Chrome | HIGH | FIXED | -| W21 | 2FA login flow crashes (SecondaryAuthFlow ErrorBoundary) | HIGH | FIXED | -| W22 | Hardcoded strings audit across entire app (centralize to i18n) | MEDIUM | FIXED | -| W23 | Terms/Privacy policy pages need real content | LOW | PLACEHOLDER DONE | -| W24 | Settings page duplicates enrollment features (removed) | LOW | DONE | -| W25 | Dashboard "from" text not translated | LOW | FIXED | -| W26 | Notification dates need proper formatting | LOW | FIXED | - ---- - -## Feature Parity (Updated end of session) - -| Feature | Web-App | Client-Apps | Gap | -|---------|---------|-------------|-----| -| Face biometrics | FULL | FULL | None | -| Voice biometrics | FULL | FULL | None | -| NFC (TC Kimlik) | Web NFC only | FULL native | Client WINS | -| FIDO2/WebAuthn | FULL | FULL | None | -| TOTP/OTP | FULL | FULL | None | -| QR Code | FULL | FULL | None | -| Multi-step auth | FULL | DONE | None | -| Verification pipeline | FULL | DONE | None | -| Voice search | FULL | DONE | None | -| Step-up auth | FULL | DONE | None | -| Role management | FULL | DONE | None | -| Audit log admin | FULL | DONE | None | -| Developer portal | FULL | DONE | None | -| Widget demo | FULL | DONE | None | - ---- - -## Completion Tracking - -- [x] Phase 1 complete — 2026-04-04 -- [x] Phase 2 complete (8/8) — 2026-04-04 -- [x] Phase 3 complete (7/7) — 2026-04-04 -- [ ] Phase 4 (maintenance) -- [x] Web-app backlog W20-W26 all cleared (W24 done 2026-04-05) -- [x] 2FA login properly working -- [x] 100% feature parity achieved diff --git a/docs/MOBILE_TESTING_GUIDE.md b/docs/MOBILE_TESTING_GUIDE.md index a5c042d7..ccf23db5 100644 --- a/docs/MOBILE_TESTING_GUIDE.md +++ b/docs/MOBILE_TESTING_GUIDE.md @@ -165,4 +165,4 @@ server **200** — always check the server verdict before assuming the API faile ## 8. Reference - `README.md`, `CLAUDE.md` — architecture & conventions. - `docs/RELEASE.md`, `docs/SIGNING.md` — signing & release. -- `ROADMAP_CLIENT_APPS.md` — roadmap. +- Open GitHub issues — current backlog / roadmap. diff --git a/docs/MOBILE_TRIAGE_2026-06-03.md b/docs/MOBILE_TRIAGE_2026-06-03.md deleted file mode 100644 index 16dc682c..00000000 --- a/docs/MOBILE_TRIAGE_2026-06-03.md +++ /dev/null @@ -1,97 +0,0 @@ -# Mobile App Triage — 2026-06-03 - -Investigation of user-reported issues on the **released v5.3.0 APK** (`origin/main`, -tag `v5.3.0` = commit `65d33306`). Each issue was traced through the mobile KMP code -(`shared/` + `androidApp/`), the web app (`web-app`, `main`), and the backend -(`identity-core-api`, `main`). - -## ⚠️ First: confirm the installed APK build - -The `v5.3.0` tag points at commit `65d33306` (PR #80), which **already contains** the -NFC-MRZ camera fix and the QR-screen scroll/nav-bar-inset fix. If the demo phone runs -a build older than that tag, issues #8 and #9 below are stale and disappear on a fresh -rebuild from the tag. **Verify the phone's APK is built from `v5.3.0` (`65d33306`) or -later.** - -## Status legend -`FIXED-HERE` shipped in this branch · `FIXED-IN-5.3.0` already in the released code · -`DEFERRED` follow-up (needs more than a mobile-only quick win) · `BY-DESIGN` working as -intended. - -| # | Issue | Root cause | Status | -|---|-------|-----------|--------| -| 1 | "My Invitations" crashes with a JSON/array decode error | Mobile calls `GET /api/v1/invites/received`; **no such backend endpoint** → 404 object decoded as a list → serializer throws. Mobile is ahead of the API. | **FIXED-HERE** | -| 2 | Activity History always empty | Screen is a hardcoded `emptyList()` (`ActivityHistoryScreen.kt:61`); never calls the API. Correct endpoint `GET /api/v1/my/activity` **exists** and the web app uses it. | **FIXED** (#83) | -| 3 | Settings: dead/misleading controls | "Enable Notifications" + "Biometric Authentication" + "Analytics" switches are local no-ops (two default ON). The "Voice, Voice Search, OTP, TOTP, Liveness, Card, Token" line is a static string advertising 6 methods the app no longer ships, mislabeled under "Biometric Authentication". | **FIXED-HERE** | -| 4 | Notifications always empty | Hardcoded `emptyList()` (`NotificationsScreen.kt:40`). **No notifications-feed endpoint exists** on the backend (only notification *preferences*). | **FIXED-HERE** (bell hidden) | -| 5 | "Login requests" inaccessible from web | Backend + mobile approver + web initiator panel are all built, but the web launch button was removed in PR #141 and **never re-homed**, so nothing can start a request → mobile screen always empty. | **FIXED** (web #199 re-homed the button) | -| 6 | "Scan ID Card / Capture Front" — what is it? | The "Add Card" photo wizard (`CardScanScreen.kt`). It photographs the card, said "Card Added Successfully", then **discards the images** — no OCR, no upload, no backend. | **FIXED-HERE** (honest copy) | -| 7 | QR login "not working because of mobile" | Two unrelated QR systems. Cross-device QR login (phone scans desktop) has a complete mobile+API half but the **web/desktop screen that shows the login QR was never built**. The web's only QR is a same-device MFA step. | **PARTIAL** (web #199 built the desktop QR; multi-step handoff deferred) | -| 8 | "Scan with MRZ" disabled | Button requires the 3 MRZ fields; v5.3.0 added a "Scan MRZ with camera" button that auto-fills them (PR #80). | **FIXED-IN-5.3.0** | -| 9 | QR bottom button invisible | QR screen now scrolls + has nav-bar insets (PR #80); button is reachable (may be below the fold). | **FIXED-IN-5.3.0** | -| 10 | "Join a Tenant" / "Members: 0" | Honest read-only directory; self-service join genuinely doesn't exist. "Members: 0" is real data (tenants the user isn't in, scoped by the tenant filter), not a bug. | **BY-DESIGN** | - -## What this branch changed (demo-safe, mobile-only) - -1. **`InviteApiImpl.getReceivedInvites()`** → returns `emptyList()` (the endpoint - doesn't exist; the screen has a proper empty state). Kills the crash. -2. **`SettingsScreen.kt`** → removed the three dead switches; Security card now shows - only the wired Change Password; auth card retitled "Authentication" with subtitle - "Authenticator app (TOTP)". -3. **`StringResources.kt`** → new `SETTINGS_AUTH_SECTION` key (EN "Authentication" / TR - "Kimlik Doğrulama"); honest `SETTINGS_AUTH_METHODS_SUB`; "Card Added*" → "Photos - Captured (preview only)" (EN + TR). -4. **`DashboardScreen.kt`** → notifications bell hidden (no backend feed). - -> All mobile fixes require an **APK rebuild + reinstall** to reach the phone. - -## Resolution (2026-06-03) - -The scope-B follow-ups were built and shipped the same day: - -- **#2 Activity History — DONE (client-apps #83, merged).** New `getMyActivity(page,size)` - on the audit API/repository hitting `GET /api/v1/my/activity`, a new - `ActivityHistoryViewModel`, action→chip mapping, loading + Retry-on-error states. - Reaches devices on the next APK rebuild. -- **#5 Approve-login — DONE (web-app #199, merged + deployed).** `Layer1Shortcuts` - renders the "Approve on another device" button again (both `LoginPage` and - `HostedLoginApp` already passed `onApproveClick`; the panel self-collects the email). - So a web sign-in can finally surface a request on the mobile "Login requests" screen. -- **#7 Cross-device QR login — PARTIAL (web-app #199, merged + deployed).** New - `qr-login.ts` + `QrLoginPanel` on both surfaces; the QR encodes the **sessionId** - (`fivucsas://qr-login?session=` — what the mobile scanner resolves; `qrContent` - is not a lookup key). Works end-to-end for single-step tenants (browser-verified - live on app.fivucsas.com). **Remaining:** a multi-step tenant (e.g. Marmara) returns - `mfaRequired` and the panel shows "continue here" rather than bridging the - `mfaSessionToken` into the step-up flow — that handoff needs a small API change (add - the next step's `availableMethods` to the QR poll response) and was deferred to avoid - a prod API rebuild right before the demo. - -Deploy state: **app.fivucsas.com** auto-deployed via the Hostinger workflow on merge; -**verify.fivucsas.com** rebuilt + redeployed (Docker). The **mobile** fixes (scope-A + -#83) reach the phone only after an operator **APK rebuild + reinstall**. - -### APK rebuilt — 2026-06-03 (pre-demo) - -The operator APK rebuild was done: a **release-signed** APK was built from `origin/main` -via the CI `android-build.yml` (`build_type=release`) and downloaded — signature verified -`CN=FIVUCSAS, O=Marmara University` (the real keystore, **not** a debug cert), so it -**upgrades in place** over the installed v5.3.0 with no uninstall. This is what carries the -scope-A fixes (#82) + Activity History (#83) to the device. Artifact lives in the parent -repo at `client-apps-apk-v5.3.0-rebuild-2026-06-03.release/androidApp-release.apk` -(versionName 5.3.0 / versionCode 12). - -> Signing note: the keystore password is **not on the Hetzner host** (CI secrets only), so a -> *local* `./gradlew :androidApp:assembleRelease` falls back to debug signing and won't -> upgrade in place. Always rebuild release APKs via -> `gh workflow run android-build.yml -R Rollingcat-Software/client-apps -r main -f build_type=release` -> then `gh run download -n fivucsas-release-apk`. - -A full record of the broader 16-finding pre-demo sweep (incl. the genuinely-open NFC #7, -mobile token-refresh #2, and backend items #11/#15) is in the parent repo: -`USER_FINDINGS_2026-06-03.md`. - -## Still deferred (need new backend endpoints) - -- **#4 Notifications feed** / **#1 invitations-received list** — post-demo; no backend - endpoint exists yet. diff --git a/docs/TODO.md b/docs/TODO.md deleted file mode 100644 index d9bae244..00000000 --- a/docs/TODO.md +++ /dev/null @@ -1,202 +0,0 @@ -# client-apps TODO - -> Source of truth for current sprint across Android / iOS / Desktop. Derived -> from the 2026-04-18e cross-platform deep review. See also `CHANGELOG.md`, -> `README.md` "Feature coverage matrix," and `../docs/plans/` (especially -> `PATH_TO_20_20.md`, `CLIENT_APPS_PARITY.md`, `NFC_PUSH_APPROVAL_PROTOCOL.md`). - -**Last updated:** 2026-04-18e - ---- - -## Phase A — Android 20/20 close-out (current sprint) - -Five Android feature-parity gaps identified by the 2026-04-18e cross-platform -deep review. Android baseline is ~15/20 against the web-app 20/20 reference. -Canonical plan with file-level detail: `../docs/plans/PATH_TO_20_20.md`. - -### A1 — Passport BAC MFA integration (~2 days) - -NFC crypto stack (`PassportNfcReader` 873 LOC, `TurkishEidReader` 457, -`BacAuthentication` 502, `SecureMessaging` 470, plus Dg1/Dg2/MRZ parsers + -`CardReaderFactory`, **5,447 LOC total**) is already under -`androidApp/src/main/kotlin/com/fivucsas/mobile/android/data/nfc/`. -`NfcReadScreen.kt` (642 LOC) exists with MRZ input UI + `koinInject`. -The gap is **integration only**: `MfaFlowScreen.kt:324` still routes -`NFC_DOCUMENT` to `GenericMethodStepInput` placeholder. - -- [ ] Port `MrzScannerScreen.kt` from `practice-and-test/UniversalNfcReader` - (CameraX preview + ML Kit text recognition + ICAO MRZ line regex). -- [ ] Create `NfcStepScreen.kt` hosting `MrzScannerScreen` → BAC key - derivation → `PassportNfcReader.read()` → DG1/DG2 parse → server - `/api/v1/mfa/nfc/verify`. -- [ ] Create `NfcStepViewModel.kt` + `MrzScannerViewModel.kt`. -- [ ] Replace the `GenericMethodStepInput` dispatch at `MfaFlowScreen.kt:324`. -- [ ] MRZ capture copy in EN + TR `strings.xml`. - -#### NFC card enrollment (done) + passive auth (deferred) - -- [x] **Enroll wiring (2026-05-30).** `NfcReadScreen` now offers "Register - this card" → `EnrollNfcCardUseCase` → `POST /api/v1/nfc/enroll`. The - serial is normalized to the API-canonical UPPERHEX-no-separators form - (`normalizeCardSerial`), aligned with identity-core-api so a - mobile-enrolled card matches a web verify. Shared client: - `NfcEnrollmentApi(Impl)` + `NfcEnrollmentRepository(Impl)`. -- [x] **Passive authentication → server (CODE-COMPLETE 2026-05-30).** The - readers now surface raw `EF.SOD` + DG1 + DG2 bytes through - `NfcIdentityDocumentData`; a "Verify authenticity" action base64-encodes - them and POSTs to `/api/v1/nfc/verify-authenticity` (200 authentic / - 422 `reasonCode` / 400 missing-SOD), fail-closed. Shared - `NfcAuthenticityApi/Repository` + `VerifyNfcAuthenticityUseCase`, i18n, - tests. **Awaiting only OPERATOR:** load ICAO-PKD CSCA roots (esp. Turkey) - into the **bio container** trust dir, else verify returns - `NO_TRUST_STORE` (422). Needs physical eID/passport test cards to - end-to-end validate. -- [x] **`CscaCertificateStore` populate hook (CODE-COMPLETE).** The store was - never empty (full PEM/DER/chain loading + DS→CSCA validation already - existed); added `loadBundledRoots(context)` auto-discovery from - `assets/csca//` (called at startup) + a drop-in README. Client - DS→CSCA check stays *advisory* (server verdict authoritative). OPERATOR: - drop Turkey CSCA root into `assets/csca/TUR/`. -- [~] **PACE** (read `EF.CardAccess`, GM mapping, AES secure messaging) — - 2026-05-30: `CardAccessParser` (parses PACEInfo, unit-tested) + - `PaceKeyDerivation` (TR-03110/9303 KDF, **vector-tested byte-exact** - against the published ICAO 9303 worked example) + `PaceAuthenticator` - (protocol selection + `K_π` derivation + BAC-fallback seam). Remaining: - the on-card GM handshake (MSE:Set AT + GENERAL AUTHENTICATE) + AES secure - messaging — needs a physical PACE card. Plan in `docs/NFC_PACE_PLAN.md`. - OPERATOR: PACE-capable test card(s) + BSI TR-03110 / 9303-11 Appendix G - GM worked-example vectors. - -### A2 — GDPR/KVKK export mobile UI (~2 days) - -Backend `GET /users/{id}/export` shipped 2026-04-16b. Web-app wired -2026-04-18 on `MyProfilePage`. Android has **zero UI**. - -- [ ] New `GdprRepository` in `data/repository/` hitting the `/export` endpoint. -- [ ] New `GdprViewModel` in `shared/presentation/viewmodel/`. -- [ ] "Download my data" row on `ProfileScreen` with DataStore-gated rate limit. -- [ ] Android `DownloadManager` integration to persist the returned JSON to - the Downloads folder. -- [ ] 8 i18n keys (EN + TR): button label, confirmation dialog, success toast, - error toast, "Download started" notification, file-name template, - rate-limit message, KVKK disclosure. -- [ ] DI wiring in `AppModule.kt`. - -### A3 — FCM action buttons + `fivucsas://nfc-session` deep-link (~2 days) - -Current `FivucsasFirebaseMessagingService` shows plain notifications. The -`AndroidManifest.xml` has only the `TECH_DISCOVERED` intent-filter. Protocol -spec already exists in `../docs/plans/NFC_PUSH_APPROVAL_PROTOCOL.md` (Ed25519 -device registration, FCM/APNS push payload, V39 migration sketch, 13-threat -security review). - -- [ ] Allow / Deny `NotificationCompat.Action` buttons on push notifications - built in `FivucsasFirebaseMessagingService`. -- [ ] New `ApprovalActionReceiver` (BroadcastReceiver) POSTing signed - approval to `/api/v1/nfc/approve` or `/deny`. -- [ ] Add `fivucsas://nfc-session` intent-filter to `AndroidManifest.xml` - (`android:autoVerify="false"`). -- [ ] `MainActivity.onNewIntent` handler parsing the session-id path segment - and navigating to `NfcStepScreen`. -- [ ] Ed25519 signature helper per protocol spec (shared `commonMain`). - -### A4 — Dark mode toggle in Settings (~1 day) - -`AppColors.kt` already exposes both palettes. Theme is driven by -`isSystemInDarkTheme()` with no user override; Settings has no theme row. - -- [ ] Add `ThemeMode { SYSTEM, LIGHT, DARK }` enum in - `shared/presentation/state/`. -- [ ] New `ThemePreferences` backed by DataStore. -- [ ] Expose via `CompositionLocalOf` so `FivucsasTheme` can - resolve the effective palette. -- [ ] 3-radio row on `SettingsScreen` ("Follow system / Light / Dark") with - live preview. -- [ ] 4 i18n keys (EN + TR): section label, follow-system, light, dark. - -### A5 — Authenticator QR scanner (~1 day) - -v5.1.0 shipped manual entry only. The bottom-sheet "Scan QR" entry is -currently a `Toast`. Existing `QrScannerScreen` (CameraX + ML Kit barcode) -is already in the codebase for the QR-code auth method. `OtpauthUri.parse()` -is already implemented in `shared/commonMain/.../authenticator/totp/`. - -- [ ] Create `OtpQrScannerScreen.kt` reusing `QrScannerScreen` CameraX + ML - Kit pipeline, filtering `BARCODE_FORMAT_QR_CODE`. -- [ ] Pipe raw text through `OtpauthUri.parse()` and dispatch a - `ScannedAccount` event up to `AuthenticatorViewModel.addAccount()`. -- [ ] Replace the `Toast` fallback in the "Scan QR" bottom-sheet branch of - `AuthenticatorScreen.kt` with a navigation call. -- [ ] 3 i18n keys (EN + TR): scanner hint, permission-denied, invalid-uri. - ---- - -## Phase B — iOS Xcode project scaffold + Apple Developer enrollment - -iOS HMAC actuals are the first blocker between v5.1.0 and an iOS-shippable -TOTP authenticator. Full iOS parity tracked under Phase 2 of -`../docs/plans/CLIENT_APPS_PARITY.md`. - -- [ ] iOS and Desktop `hmacSha1` / `hmacSha256` / `hmacSha512` actuals. - Current `iosMain` stubs throw `TODO()`. Android + Desktop actuals use - `javax.crypto.Mac` and are fully functional. -- [ ] Xcode project scaffold reusing the KMP `shared` framework (xcframework - export already wired in `build.gradle.kts`). -- [ ] Apple Developer Program enrollment (organisation account, - $99 USD / year). -- [ ] iOS WebAuthn via `ASAuthorizationPlatformPublicKeyCredentialProvider` - (drop-in for the Android Credential Manager wrapper). -- [ ] iOS NFC reader via `CoreNFC` (parity with Android - `PassportNfcReader`); ICAO MRTD chip read (DG1 MRZ, DG2 face). -- [ ] iOS hosted-login handoff via `ASWebAuthenticationSession` + AppAuth. -- [ ] Settings + Profile + GDPR export UI parity (reuse shared ViewModels). - ---- - -## Phase C — Desktop installer signing - -Desktop JVM builds are unsigned today. Full Desktop parity tracked under -Phase 3 of `../docs/plans/CLIENT_APPS_PARITY.md`. - -- [ ] Desktop `hmacSha1` / `hmacSha256` / `hmacSha512` actuals (shared with - Phase B item 1). -- [ ] Desktop NFC reader via PC/SC (`javax.smartcardio`). -- [ ] Windows Authenticode signing with EV code-signing certificate - (SmartScreen reputation requires EV). -- [ ] macOS notarization via `notarytool` (hardened runtime + stapled - ticket). -- [ ] Linux AppImage + `.deb` build via Compose Multiplatform's `packageDeb` - / `packageAppImage`. -- [ ] Desktop GDPR export row + Settings dark-mode row parity (shared - ViewModels land with Phase A2 + A4). - ---- - -## Phase D — Test burn-down - -- [ ] Fix pre-existing `BiometricViewModelTest.enrollFace` failure on - `client-apps`. Known red test; does not block 20/20 or v5.2.0 tag. - Root cause investigation: is it fake-service drift or an actual - ViewModel regression? -- [ ] Compose UI test for `AuthenticatorScreen` (account renders, code - updates on tick, tap copies to clipboard). Requires a running AVD in - CI which is not wired in this environment. -- [ ] Compose UI test for `NfcStepScreen` + `MrzScannerScreen` (delivered - under Phase A1) — same AVD-in-CI blocker. -- [ ] Biometric gate on Authenticator reveal / pull-to-refresh - (deliberately omitted in v5.1.0; revisit after day-to-day use). - ---- - -## Phase E — BYOD + localisation follow-ups - -- [ ] BYOD architecture (tenants host their own biometric store) — 8-week - lift per `../docs/plans/BYOD_ARCHITECTURE.md`. Book after parent - `ROADMAP.md` Phase A–F are green. -- [ ] Third-language localisation stubs (Arabic RTL, German, French) — no - hardcoded strings today, but `en.json` + `tr.json` key parity needs a - CI linter before new locales land. -- [ ] Accessibility sweep: `aria-describedby` equivalents (`semantics - contentDescription =`), minimum touch-target audit on Compose - components (48.dp floor), TalkBack / VoiceOver test pass. diff --git a/docs/archive/2026-04-16/CAMERA_IMPLEMENTATION_SUMMARY.md b/docs/archive/2026-04-16/CAMERA_IMPLEMENTATION_SUMMARY.md deleted file mode 100644 index b3e0bcc5..00000000 --- a/docs/archive/2026-04-16/CAMERA_IMPLEMENTATION_SUMMARY.md +++ /dev/null @@ -1,433 +0,0 @@ -# Camera Integration Implementation Summary - -## Overview -Comprehensive camera integration for FIVUCSAS mobile and desktop applications following Hexagonal Architecture, SOLID principles, and professional software engineering practices. - -## Implementation Status: COMPLETE - -All requested tasks have been successfully implemented: -- ✅ Enhanced ICameraService interface with preview and state management -- ✅ Created CameraState sealed class for state management -- ✅ Implemented AndroidCameraService with CameraX -- ✅ Enhanced DesktopCameraServiceImpl with JavaCV integration -- ✅ Created CameraPreview composable for common UI -- ✅ Created platform-specific camera UI implementations -- ✅ Created PlatformModule for camera service DI configuration -- ✅ Updated AppModule to include platform modules -- ✅ Added camera permission handling for Android -- ✅ Created comprehensive documentation - -## Files Created/Modified - -### Core Interfaces (Common) -1. **`shared/src/commonMain/kotlin/com/fivucsas/shared/platform/ICameraService.kt`** (Modified) - - Enhanced with preview control methods - - Added StateFlow for reactive state management - - Added LensFacing enum for camera selection - - Added camera availability checks - - Added resolution query methods - -2. **`shared/src/commonMain/kotlin/com/fivucsas/shared/platform/CameraState.kt`** (New) - - Sealed class representing all camera states - - States: Idle, Initializing, Ready, Previewing, Capturing, Error, Released - - Type-safe state management - -### Android Implementation -3. **`shared/src/androidMain/kotlin/com/fivucsas/shared/platform/AndroidCameraService.kt`** (New) - - CameraX-based implementation - - Lifecycle-aware camera management - - High-quality image capture (JPEG 95%) - - Preview support with PreviewView - - Frame analysis capability - - Front/back camera support - - Proper resource cleanup - -4. **`shared/src/androidMain/kotlin/com/fivucsas/shared/ui/platform/AndroidCameraPreview.kt`** (New) - - Composable for Android camera preview - - AndroidView integration with CameraX - - PreviewView embedding - -5. **`shared/src/androidMain/kotlin/com/fivucsas/shared/platform/CameraPermissionHelper.kt`** (New) - - Camera permission handling - - Accompanist Permissions integration - - Permission state management - - Permission checker utilities - -### Desktop Implementation -6. **`desktopApp/src/desktopMain/kotlin/com/fivucsas/desktop/platform/DesktopCameraServiceImpl.kt`** (Modified) - - JavaCV/OpenCV integration - - Webcam access and frame grabbing - - BufferedImage to ByteArray conversion - - 30 FPS frame capture - - Multiple camera support - -7. **`desktopApp/src/desktopMain/kotlin/com/fivucsas/desktop/ui/DesktopCameraPreview.kt`** (New) - - Composable for Desktop camera preview - - Continuous frame capture and display - - BufferedImage to Compose ImageBitmap conversion - -### UI Components (Common) -8. **`shared/src/commonMain/kotlin/com/fivucsas/shared/ui/components/organisms/CameraPreview.kt`** (New) - - CameraPreviewContainer: Common camera UI structure - - FaceDetectionOverlay: Visual guidance for face positioning - - State-based UI rendering - - Capture, close, and flip camera controls - - Error handling UI - -### Dependency Injection -9. **`shared/src/commonMain/kotlin/com/fivucsas/shared/di/PlatformModule.kt`** (New) - - expect/actual pattern for platform modules - - Common interface for platform-specific DI - -10. **`shared/src/androidMain/kotlin/com/fivucsas/shared/di/PlatformModule.android.kt`** (New) - - Android-specific Koin module - - Provides AndroidCameraService - - Provides AndroidTokenStorage - -11. **`shared/src/desktopMain/kotlin/com/fivucsas/shared/di/PlatformModule.desktop.kt`** (New) - - Desktop-specific Koin module - - Provides DesktopCameraServiceImpl - -12. **`shared/src/iosMain/kotlin/com/fivucsas/shared/di/PlatformModule.ios.kt`** (New) - - iOS platform module stub - - Ready for future iOS implementation - -13. **`shared/src/commonMain/kotlin/com/fivucsas/shared/di/AppModule.kt`** (Modified) - - Added platformModule to includes - - Enhanced documentation - -### Documentation -14. **`mobile-app/CAMERA_INTEGRATION_GUIDE.md`** (New) - - Comprehensive usage guide - - Architecture overview - - Component descriptions - - Usage examples - - Configuration guide - - Best practices - - Troubleshooting - -15. **`mobile-app/CAMERA_IMPLEMENTATION_SUMMARY.md`** (New) - - This file - - Implementation summary - - Architecture overview - - File listing - -## Architecture - -### Hexagonal Architecture (Ports and Adapters) - -``` -┌────────────────────────────────────────────────────────────┐ -│ Application Core │ -│ (Domain Layer - Use Cases) │ -│ - EnrollUserUseCase │ -│ - VerifyUserUseCase │ -│ - CheckLivenessUseCase │ -└───────────────────────┬────────────────────────────────────┘ - │ - │ depends on (Port) - ↓ -┌────────────────────────────────────────────────────────────┐ -│ ICameraService Interface (Port) │ -│ + initialize(lensFacing): Result │ -│ + startPreview(): Result │ -│ + stopPreview(): Result │ -│ + captureImage(): Result │ -│ + captureFrame(): Result │ -│ + cameraState: StateFlow │ -│ + isAvailable(): Boolean │ -│ + hasCamera(lensFacing): Boolean │ -│ + release() │ -└─────────┬──────────────────────┬───────────────────────────┘ - │ │ - │ │ - ┌──────↓─────────┐ ┌─────↓──────────────┐ - │ Android │ │ Desktop │ - │ Adapter │ │ Adapter │ - ├────────────────┤ ├────────────────────┤ - │ AndroidCamera │ │ DesktopCamera │ - │ Service │ │ ServiceImpl │ - │ │ │ │ - │ Uses: │ │ Uses: │ - │ - CameraX │ │ - JavaCV │ - │ - PreviewView │ │ - OpenCV │ - │ - ImageCapture │ │ - FrameGrabber │ - └────────────────┘ └────────────────────┘ -``` - -## SOLID Principles Applied - -### 1. Single Responsibility Principle (SRP) -- **ICameraService**: Only handles camera operations -- **CameraState**: Only represents camera state -- **AndroidCameraService**: Only handles Android camera implementation -- **DesktopCameraServiceImpl**: Only handles desktop camera implementation -- **CameraPermissionHelper**: Only handles camera permissions - -### 2. Open/Closed Principle (OCP) -- **ICameraService** is open for extension (new platforms) but closed for modification -- New platforms can be added by implementing ICameraService without changing existing code -- CameraState sealed class can be extended with new states - -### 3. Liskov Substitution Principle (LSP) -- Any implementation of ICameraService can be substituted without breaking the application -- AndroidCameraService and DesktopCameraServiceImpl are fully interchangeable -- All implementations honor the contract defined by ICameraService - -### 4. Interface Segregation Principle (ISP) -- ICameraService contains only methods relevant to camera operations -- No unnecessary methods that implementations must provide -- Focused, cohesive interface - -### 5. Dependency Inversion Principle (DIP) -- High-level modules (use cases, ViewModels) depend on ICameraService abstraction -- Low-level modules (platform implementations) depend on ICameraService abstraction -- No direct dependency on concrete implementations -- Dependency injection via Koin - -## Design Patterns Used - -### 1. Hexagonal Architecture (Ports and Adapters) -- **Port**: ICameraService interface -- **Adapters**: AndroidCameraService, DesktopCameraServiceImpl -- Core business logic is isolated from platform details - -### 2. Adapter Pattern -- AndroidCameraService adapts CameraX to ICameraService -- DesktopCameraServiceImpl adapts JavaCV to ICameraService - -### 3. State Pattern -- CameraState sealed class represents different camera states -- Clean state transitions and type-safe state handling - -### 4. Observer Pattern -- StateFlow for reactive state observation -- UI components observe camera state changes - -### 5. Template Method Pattern -- CameraPreviewContainer defines the camera UI structure -- Platform-specific implementations fill in the details - -### 6. Strategy Pattern -- Different camera strategies for different platforms -- Interchangeable at runtime via dependency injection - -### 7. Factory Pattern -- Platform modules act as factories for creating camera service instances -- Koin provides the factory mechanism - -### 8. Dependency Injection -- Koin provides implementations based on platform -- Loose coupling between components - -### 9. Composite Pattern -- CameraPreviewContainer composes multiple UI elements -- Overlay, controls, and preview combined into cohesive UI - -### 10. Bridge Pattern -- AndroidCameraPreview bridges Compose UI with Android View system -- Separates abstraction (Compose) from implementation (View) - -## Key Features - -### Camera Management -- ✅ Initialize/release camera -- ✅ Start/stop preview -- ✅ High-quality image capture -- ✅ Quick frame capture -- ✅ Front/back camera support -- ✅ Camera availability checks -- ✅ State management via Flow - -### Android Features -- ✅ CameraX integration -- ✅ Lifecycle-aware management -- ✅ Permission handling with Accompanist -- ✅ PreviewView integration -- ✅ ImageCapture for high quality -- ✅ ImageAnalysis for frame processing -- ✅ Auto-focus support -- ✅ 95% JPEG quality - -### Desktop Features -- ✅ JavaCV/OpenCV integration -- ✅ Cross-platform webcam access -- ✅ BufferedImage processing -- ✅ 30 FPS frame capture -- ✅ Multiple camera support -- ✅ JPEG encoding - -### UI Features -- ✅ State-based rendering -- ✅ Loading indicators -- ✅ Error messages -- ✅ Capture button -- ✅ Close button -- ✅ Flip camera button -- ✅ Face detection overlay -- ✅ Guidance text -- ✅ Responsive layouts - -## Configuration - -### Biometric Configuration -Location: `shared/src/commonMain/kotlin/com/fivucsas/shared/config/BiometricConfig.kt` - -Camera settings optimized for biometric processing: -```kotlin -const val PREFERRED_IMAGE_WIDTH = 640 -const val PREFERRED_IMAGE_HEIGHT = 480 -const val CAMERA_FRAME_RATE_FPS = 30 -const val CAMERA_AUTO_FOCUS_ENABLED = true -const val MIN_FACE_SIZE_PIXELS = 100 -const val MAX_FACE_SIZE_PIXELS = 500 -``` - -## Dependencies - -### Already Configured - -#### Android (androidApp/build.gradle.kts) -```kotlin -// CameraX -implementation("androidx.camera:camera-core:1.3.0") -implementation("androidx.camera:camera-camera2:1.3.0") -implementation("androidx.camera:camera-lifecycle:1.3.0") -implementation("androidx.camera:camera-view:1.3.0") - -// Permissions -implementation("com.google.accompanist:accompanist-permissions:0.32.0") -``` - -#### Desktop (desktopApp/build.gradle.kts) -```kotlin -// JavaCV for webcam -implementation("org.bytedeco:javacv-platform:1.5.10") -``` - -#### Common (shared/build.gradle.kts) -```kotlin -// Koin -implementation("io.insert-koin:koin-core:3.5.0") -implementation("io.insert-koin:koin-compose:1.1.0") - -// Coroutines -implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") -``` - -## Usage Example - -```kotlin -@Composable -fun BiometricCaptureScreen( - cameraService: ICameraService = get() // Injected by Koin -) { - val cameraState by cameraService.cameraState.collectAsState() - val scope = rememberCoroutineScope() - - LaunchedEffect(Unit) { - cameraService.initialize(LensFacing.FRONT) - cameraService.startPreview() - } - - DisposableEffect(Unit) { - onDispose { - scope.launch { cameraService.release() } - } - } - - CameraPreviewContainer( - cameraState = cameraState, - onCaptureClick = { - scope.launch { - cameraService.captureImage() - .onSuccess { imageBytes -> - // Process captured image - } - } - }, - onCloseClick = { /* Navigate back */ }, - overlayContent = { - FaceDetectionOverlay( - showGuide = true, - guidanceText = "Position your face" - ) - } - ) { - // Platform-specific preview - } -} -``` - -## Testing Strategy - -### Unit Tests -- Mock ICameraService for business logic tests -- Test camera state transitions -- Test error handling - -### Integration Tests -- Test camera initialization on real devices -- Test image capture quality -- Test preview rendering - -### Platform Tests -- Android: Test CameraX integration -- Desktop: Test JavaCV integration -- Test camera availability detection - -## Benefits - -### For Developers -1. **Easy to Test**: Mock ICameraService for unit tests -2. **Type Safe**: Kotlin sealed classes and type system -3. **Clear Contracts**: Well-defined interfaces -4. **Platform Independent**: Write once, run anywhere -5. **Maintainable**: Clean separation of concerns -6. **Extensible**: Easy to add new platforms - -### For Users -1. **Reliable**: Robust error handling -2. **Responsive**: Reactive state management -3. **User-Friendly**: Clear visual feedback -4. **High Quality**: Optimized for biometric processing -5. **Consistent**: Same UX across platforms - -### For Business -1. **Faster Development**: Reusable components -2. **Lower Costs**: Shared code reduces duplication -3. **Better Quality**: Professional architecture -4. **Easier Maintenance**: Well-documented and structured -5. **Future-Proof**: Easy to extend and modify - -## Next Steps - -### Immediate -1. ✅ All core functionality implemented -2. ✅ Documentation completed -3. Test on physical devices -4. Gather user feedback - -### Short Term -1. Implement iOS camera service using AVFoundation -2. Add face detection ML model integration -3. Add liveness detection -4. Optimize performance - -### Long Term -1. Add advanced camera features (HDR, night mode) -2. Implement video recording capability -3. Add real-time face tracking -4. Support multiple face detection - -## Conclusion - -This implementation provides a production-ready, professional camera integration for FIVUCSAS that: -- Follows industry best practices -- Applies SOLID principles consistently -- Uses proven design patterns -- Provides excellent testability -- Offers great user experience -- Is maintainable and extensible - -The architecture ensures that the application can easily adapt to new requirements, platforms, and technologies while maintaining code quality and reliability. diff --git a/docs/archive/2026-04-16/CAMERA_QUICK_REFERENCE.md b/docs/archive/2026-04-16/CAMERA_QUICK_REFERENCE.md index 2d972a4e..609b7b91 100644 --- a/docs/archive/2026-04-16/CAMERA_QUICK_REFERENCE.md +++ b/docs/archive/2026-04-16/CAMERA_QUICK_REFERENCE.md @@ -291,5 +291,4 @@ fun setup() { For detailed information, see: - **Full Guide**: `CAMERA_INTEGRATION_GUIDE.md` -- **Implementation**: `CAMERA_IMPLEMENTATION_SUMMARY.md` - **Code**: Browse implementation files diff --git a/docs/archive/2026-04-16/IMPLEMENTATION_NOTES.md b/docs/archive/2026-04-16/IMPLEMENTATION_NOTES.md deleted file mode 100644 index 18a18ecc..00000000 --- a/docs/archive/2026-04-16/IMPLEMENTATION_NOTES.md +++ /dev/null @@ -1,205 +0,0 @@ -# Implementation Notes - -**Last Updated:** February 2, 2026 -**Branch:** `feature/glsm/backend-integration` - ---- - -## Recent Changes - -### Biometric Enroll Screen — Full Refactor - -**Date:** February 2, 2026 -**Files Changed:** - -| File | Action | -|------|--------| -| `androidApp/.../ui/screen/BiometricEnrollScreen.kt` | Rewritten | -| `androidApp/.../ui/screen/BiometricVerifyScreen.kt` | Rewritten | -| `androidApp/.../ui/util/ImageCaptureUtils.kt` | Created | -| `shared/.../viewmodel/auth/BiometricViewModel.kt` | Modified | -| `shared/.../usecase/enrollment/EnrollUserUseCase.kt` | Modified | - -**What changed:** - -1. **Enrollment form added** — Two-step flow: Form (personal info) then Camera capture. Replaces previously hardcoded `"Test User"` / `"test@example.com"` enrollment data. All fields are optional for development/testing — validation is advisory, not blocking. - -2. **Photo preview step** — After capture, user sees a mirrored preview of their photo and can choose to **Submit** or **Retake**. The non-mirrored original bytes are sent to the backend. Flow: `Form → Camera → Preview → Submit → Success`. - -3. **Camera capture fixed** — Removed broken custom `ImageProxy.toBitmap()` extension that only worked with JPEG format. Created `ImageCaptureUtils.kt` using CameraX 1.3+ built-in `toBitmap()` which handles all device-specific image formats (YUV_420_888, JPEG, etc.). All intermediate bitmaps are properly recycled. - -4. **Error handling** — `ImageCaptureException` is now reported to the UI via `BiometricViewModel.onCaptureError()`. Both screens show error cards with "Try Again" buttons. - -5. **Camera resource cleanup** — `DisposableEffect { onDispose { cameraController.unbind() } }` added to both screens to prevent camera resource leaks on navigation. - -6. **Permission handling** — Detects permanent denial (`shouldShowRationale == false` after request) and shows "Open App Settings" button instead of a non-functional "Grant Permission" button. - -7. **National ID made optional** — Both UI validation (`BiometricEnrollScreen`) and backend validation (`EnrollUserUseCase`) now skip national ID if left blank. Same treatment for all other fields. - -**Architectural decisions:** - -- Form state is local to the composable (`remember`), not in the ViewModel. Form data is a UI concern; the ViewModel only handles API calls (SRP). -- `ImageCaptureUtils.kt` is shared between enroll and verify screens to avoid duplicate bitmap logic (DRY). -- The `BiometricEnrollScreen` function signature is unchanged (`userId`, `viewModel`, `onNavigateBack`) for backward compatibility. `userId` parameter is currently unused but kept to preserve the navigation route contract. -- Step tracking uses `rememberSaveable` with string constants to survive configuration changes. - -**Known limitations:** - -- `BiometricViewModel` does not extend `androidx.lifecycle.ViewModel` — state is lost on configuration change. Requires KMP multiplatform lifecycle dependency upgrade. -- `LocalLifecycleOwner.current` is deprecated in newer Compose versions. Migration to Lifecycle 2.8+ API deferred. -- `Icons.Default.ArrowBack` and `Icons.Default.Send` show deprecation warnings (should use `AutoMirrored` variants). Non-functional issue. - ---- - -## Future Plan - -### FP-1: Session Manager — Auto-fill Enrollment Form from Logged-in User - -**Priority:** HIGH -**Depends on:** Auth flow storing user data after login - -**Problem:** -After login, the app only stores JWT tokens (`TokenManager`). The user's name, email, and ID are not persisted. The enrollment form cannot auto-fill from the current session. Dashboard and Profile screens also use hardcoded `"Test User"` / `"test@fivucsas.com"`. - -**Proposed solution:** - -1. Create `SessionManager` class in `shared/src/commonMain/`: - ``` - class SessionManager(private val secureStorage: ISecureStorage) { - fun saveUserSession(user: User) - fun getCurrentUser(): User? - fun getCurrentUserEmail(): String? - fun getCurrentUserName(): String? - fun clearSession() - } - ``` - Storage keys already exist in `ISecureStorage.StorageKeys` (`USER_ID`, `USER_EMAIL`) but are unused. - -2. Update `AuthRepositoryImpl` — after successful login, call `SessionManager.saveUserSession()` with the user data from the JWT token claims or a `/auth/me` API call. - -3. Update `BiometricEnrollScreen` — inject `SessionManager` via Koin, pre-fill form fields from `sessionManager.getCurrentUser()` on initial load. - -4. Update `AppNavigation.kt` — replace hardcoded `userName = "Test User"` and `userEmail = "test@fivucsas.com"` with values from `SessionManager`. - -5. Register `SessionManager` as a singleton in `AppModule.kt`. - -**Files to create/modify:** -- `shared/.../data/local/SessionManager.kt` — Create -- `shared/.../data/repository/AuthRepositoryImpl.kt` — Modify (save user on login) -- `shared/.../di/AppModule.kt` — Modify (register SessionManager) -- `androidApp/.../ui/screen/BiometricEnrollScreen.kt` — Modify (pre-fill form) -- `androidApp/.../ui/navigation/AppNavigation.kt` — Modify (use real user data) - ---- - -### FP-2: Liveness Detection in Enrollment Flow - -**Priority:** HIGH -**Depends on:** Backend biometric API liveness endpoint - -**Problem:** -The infrastructure for liveness detection exists (`CheckLivenessUseCase`, `BiometricRepository.checkLiveness()`, `BiometricConfig.LIVENESS_THRESHOLD`) but is not wired into the enrollment or verification flows. A static photo can pass enrollment. - -**Proposed solution:** -Add a liveness check step between photo capture and enrollment submission. Options: -- **Server-side liveness:** Send the captured image to the `/biometric/liveness` endpoint before enrollment. Simplest approach, requires backend support. -- **Client-side liveness:** Use ML Kit Face Detection to require actions (blink, smile, turn head) before capturing. More secure but requires adding the ML Kit dependency. - -**Recommended approach:** Start with server-side liveness (minimal client changes), add client-side as a second layer later. - ---- - -### FP-3: Client-side Image Quality Checks - -**Priority:** MEDIUM - -**Problem:** -`BiometricConfig` defines quality thresholds (brightness, sharpness, blur) but no client-side checks are performed. Poor quality images are sent to the backend, wasting API calls and causing failed enrollments. - -**Proposed solution:** -After capture (in the preview step), analyze the bitmap before allowing submission: -- **Brightness check:** Calculate average pixel luminance, reject if below `MIN_BRIGHTNESS` or above `MAX_BRIGHTNESS`. -- **Blur detection:** Apply Laplacian variance calculation on the bitmap. Reject if below `MIN_SHARPNESS`. -- Show user-friendly feedback: "Photo is too dark — try again with better lighting." - -This fits naturally in the `PhotoPreviewContent` composable, between capture and submit. - ---- - -### FP-4: ML Kit Face Detection for Guided Capture - -**Priority:** LOW -**Dependency:** `com.google.mlkit:face-detection` - -**Problem:** -The circular face guide is purely visual. There is no validation that a face is actually present or properly positioned in the frame. - -**Proposed solution:** -Integrate ML Kit Face Detection to: -- Detect face presence in real-time during camera preview -- Show green/red frame indicator based on face position -- Require face centered within the guide before enabling the capture button -- Optionally detect face landmarks for liveness (eye open/closed, smile) - ---- - -### FP-5: ViewModel Lifecycle Awareness - -**Priority:** MEDIUM - -**Problem:** -`BiometricViewModel` is a plain Kotlin class, not `androidx.lifecycle.ViewModel`. Combined with Koin `factoryOf`, a new instance is created on every injection. State is lost on configuration changes (screen rotation). - -**Proposed solution:** -- Add `androidx.lifecycle:lifecycle-viewmodel` multiplatform dependency (available since Lifecycle 2.8+) -- Extend `BiometricViewModel` from `ViewModel` -- Change Koin registration from `factoryOf` to `viewModelOf` -- Use `koinViewModel()` instead of `koinInject()` in composables - -This affects all ViewModels in the app (`LoginViewModel`, `RegisterViewModel`, `BiometricViewModel`, etc.) and should be done as a single coordinated change. - ---- - -## Reference - -### Enrollment Screen Flow Diagram - -``` -┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ -│ │ │ │ │ │ │ │ -│ Form │────>│ Camera │────>│ Preview │────>│ Success │ -│ │ │ Capture │ │ (Review) │ │ │ -└──────────┘ └──────────────┘ └──────────────┘ └──────────┘ - ^ ^ │ │ - │ │ │ │ - │ └── Retake ──────────┘ │ - │ │ - └──────────────── Back ─────────────────────────────────────┘ -``` - -### Files Overview - -``` -androidApp/src/main/kotlin/com/fivucsas/mobile/android/ -├── ui/ -│ ├── screen/ -│ │ ├── BiometricEnrollScreen.kt ← Multi-step enrollment (form + capture + preview) -│ │ └── BiometricVerifyScreen.kt ← Camera capture + verification result -│ ├── util/ -│ │ └── ImageCaptureUtils.kt ← Shared camera bitmap utilities -│ └── navigation/ -│ └── AppNavigation.kt ← Route definitions + screen wiring -│ -shared/src/commonMain/kotlin/com/fivucsas/shared/ -├── presentation/viewmodel/auth/ -│ └── BiometricViewModel.kt ← enrollFace, verifyFace, onCaptureError -├── domain/ -│ ├── usecase/enrollment/ -│ │ └── EnrollUserUseCase.kt ← 5-step enrollment with rollback -│ ├── model/ -│ │ └── EnrollmentData.kt ← Form data model -│ └── validation/ -│ └── ValidationRules.kt ← Shared validation (name, email, TC ID, phone) -└── config/ - └── BiometricConfig.kt ← Thresholds, timeouts, quality settings -``` diff --git a/docs/archive/2026-04-16/IMPLEMENTATION_PLAN.md b/docs/archive/2026-04-16/IMPLEMENTATION_PLAN.md deleted file mode 100644 index bcb84fce..00000000 --- a/docs/archive/2026-04-16/IMPLEMENTATION_PLAN.md +++ /dev/null @@ -1,2119 +0,0 @@ -# Client Apps - Implementation Plan for 100% Completion - -**Version**: 2.0 -**Date**: January 2026 -**Target**: 100% Production-Ready -**Current Status**: ~60% Complete -**Estimated Effort**: 21 days - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Current State Analysis](#current-state-analysis) -3. [API Contracts](#api-contracts) -4. [Implementation Phases](#implementation-phases) - - [Phase 1: Package Consolidation & Code Cleanup](#phase-1-package-consolidation--code-cleanup) - - [Phase 2: Backend Integration](#phase-2-backend-integration) - - [Phase 3: Camera Integration](#phase-3-camera-integration) - - [Phase 4: Biometric Flow Implementation](#phase-4-biometric-flow-implementation) - - [Phase 5: Platform-Specific Features](#phase-5-platform-specific-features) - - [Phase 6: Test Coverage Expansion](#phase-6-test-coverage-expansion) - - [Phase 7: Production Readiness](#phase-7-production-readiness) -5. [Integration Points](#integration-points) -6. [Testing Strategy](#testing-strategy) -7. [Deployment Checklist](#deployment-checklist) - ---- - -## Executive Summary - -This document provides a comprehensive implementation plan for completing the FIVUCSAS Client Apps to 100% production readiness. The client-apps module is a Kotlin Multiplatform project supporting Android, iOS, and Desktop platforms. - -### Architecture - -``` -client-apps/ -├── shared/ # Shared Kotlin Multiplatform code -│ └── src/ -│ ├── commonMain/ # Cross-platform code -│ │ └── kotlin/com/fivucsas/shared/ -│ │ ├── config/ # Configuration constants -│ │ ├── data/ # Data layer (repositories, API) -│ │ ├── di/ # Koin dependency injection -│ │ ├── domain/ # Domain models and use cases -│ │ ├── platform/ # Platform abstractions -│ │ └── presentation/# ViewModels and UI state -│ ├── androidMain/ # Android-specific implementations -│ ├── iosMain/ # iOS-specific implementations -│ └── desktopMain/ # Desktop-specific implementations -├── androidApp/ # Android application -├── desktopApp/ # Desktop application (Compose Desktop) -└── iosApp/ # iOS application (SwiftUI wrapper) -``` - -### Key Dependencies - -- **Kotlin Multiplatform** - Cross-platform development -- **Compose Multiplatform** - UI framework -- **Koin** - Dependency injection -- **Ktor** - HTTP client -- **Kotlinx.serialization** - JSON serialization -- **Kotlinx.coroutines** - Async operations -- **CameraX** (Android) - Camera capture -- **AVFoundation** (iOS) - Camera capture -- **JavaCV** (Desktop) - Camera capture - ---- - -## Current State Analysis - -### Completed Features (60%) - -| Feature | Status | Notes | -|---------|--------|-------| -| Project Structure | ✅ 100% | KMP setup complete | -| Koin DI | ✅ 100% | Dependency injection configured | -| Mock API Mode | ✅ 100% | Development without backend | -| Admin Dashboard UI | ✅ 90% | All tabs implemented | -| Kiosk Mode UI | ✅ 85% | Welcome, Enrollment, Verification | -| Desktop App | ✅ 80% | Functional but needs polish | -| Shared Components | ✅ 70% | Some extraction needed | - -### Pending Features (40%) - -| Feature | Status | Priority | -|---------|--------|----------| -| Real API Integration | 🔴 10% | HIGH | -| Camera Integration (Android) | 🔴 20% | HIGH | -| Camera Integration (Desktop) | 🟡 40% | HIGH | -| Camera Integration (iOS) | 🔴 0% | MEDIUM | -| Biometric Capture Flow | 🔴 20% | HIGH | -| Test Coverage | 🔴 10% | HIGH | -| Error Handling | 🟡 50% | HIGH | -| Offline Support | 🔴 0% | MEDIUM | -| Package Consolidation | 🔴 0% | HIGH | - ---- - -## API Contracts - -### Identity Core API (http://localhost:8080/api/v1) - -All requests require the following headers: - -```kotlin -// Common headers interface -interface CommonHeaders { - val authorization: String // "Bearer ${accessToken}" - val tenantId: String // X-Tenant-ID header - val contentType: String // "application/json" -} -``` - -### Authentication Endpoints - -```kotlin -// POST /auth/login -@Serializable -data class LoginRequest( - val email: String, - val password: String -) - -@Serializable -data class LoginResponse( - val accessToken: String, - val refreshToken: String, - val expiresIn: Long, - val user: UserDTO -) - -// POST /auth/refresh -@Serializable -data class RefreshRequest( - val refreshToken: String -) - -@Serializable -data class RefreshResponse( - val accessToken: String, - val refreshToken: String, - val expiresIn: Long -) - -// GET /auth/me -// Returns UserDTO - -// POST /auth/logout -// Returns 204 No Content -``` - -### User Management Endpoints - -```kotlin -// GET /users?page=0&size=20&sort=createdAt,desc -@Serializable -data class PaginatedResponse( - val content: List, - val page: Int, - val size: Int, - val totalElements: Long, - val totalPages: Int -) - -@Serializable -data class UserDTO( - val id: Long, - val email: String, - val firstName: String, - val lastName: String, - val role: UserRole, - val status: UserStatus, - val tenantId: Long, - val permissions: List, - val createdAt: String, - val updatedAt: String, - val lastLoginAt: String? = null, - val lastLoginIp: String? = null -) - -@Serializable -enum class UserRole { - SUPER_ADMIN, ADMIN, OPERATOR, VIEWER -} - -@Serializable -enum class UserStatus { - ACTIVE, INACTIVE, LOCKED -} - -// POST /users -@Serializable -data class CreateUserRequest( - val email: String, - val firstName: String, - val lastName: String, - val password: String, - val role: UserRole -) - -// PUT /users/{id} -@Serializable -data class UpdateUserRequest( - val firstName: String, - val lastName: String, - val role: UserRole, - val status: UserStatus -) - -// DELETE /users/{id} -// Returns 204 No Content -``` - -### Biometric Processor API (http://localhost:8001/api/v1) - -```kotlin -// POST /enrollments -@Serializable -data class EnrollmentRequest( - val userId: Long, - val image: String, // Base64 encoded image - val metadata: EnrollmentMetadata? = null -) - -@Serializable -data class EnrollmentMetadata( - val deviceType: String, - val captureQuality: Float, - val lightCondition: String -) - -@Serializable -data class EnrollmentResponse( - val enrollmentId: String, - val status: EnrollmentStatus, - val qualityScore: Float, - val message: String -) - -@Serializable -enum class EnrollmentStatus { - PENDING, COMPLETED, FAILED -} - -// POST /verify -@Serializable -data class VerificationRequest( - val image: String, // Base64 encoded image - val userId: Long? = null, // Optional for 1:N matching - val threshold: Float = 0.85f -) - -@Serializable -data class VerificationResponse( - val verified: Boolean, - val confidence: Float, - val userId: Long? = null, - val message: String, - val processingTimeMs: Long -) - -// POST /liveness -@Serializable -data class LivenessRequest( - val image: String, // Base64 encoded image - val challenge: LivenessChallenge? = null -) - -@Serializable -data class LivenessChallenge( - val type: String, // "BLINK", "SMILE", "TURN_HEAD" - val direction: String? = null -) - -@Serializable -data class LivenessResponse( - val isLive: Boolean, - val confidence: Float, - val challengePassed: Boolean, - val message: String -) - -// GET /enrollments/{userId} -// Returns List - -@Serializable -data class EnrollmentDTO( - val id: String, - val userId: Long, - val status: EnrollmentStatus, - val qualityScore: Float, - val createdAt: String, - val expiresAt: String? = null -) -``` - -### Dashboard/Statistics Endpoints - -```kotlin -// GET /dashboard/statistics -@Serializable -data class DashboardStatistics( - val totalUsers: Int, - val activeUsers: Int, - val totalEnrollments: Int, - val totalVerifications: Int, - val recentVerifications: List, - val enrollmentsByDay: List, - val verificationsByDay: List -) - -@Serializable -data class VerificationActivity( - val id: Long, - val userId: Long, - val userName: String, - val result: Boolean, - val confidence: Float, - val timestamp: String -) - -@Serializable -data class DailyCount( - val date: String, - val count: Int -) -``` - ---- - -## Implementation Phases - -### Phase 1: Package Consolidation & Code Cleanup - -**Duration**: 2 days -**Priority**: HIGH -**Goal**: Clean up duplicate packages and establish clean architecture - -#### 1.1 Merge Duplicate Packages - -```kotlin -// Current structure has duplicates: -// com.fivucsas.mobile -> DELETE -// com.fivucsas.shared -> KEEP - -// Step 1: Find all imports from mobile package -// grep -r "import com.fivucsas.mobile" shared/ - -// Step 2: Update imports to use shared package -// Find: import com.fivucsas.mobile -// Replace: import com.fivucsas.shared -``` - -#### 1.2 Create Configuration Objects - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/config/AppConfig.kt -package com.fivucsas.shared.config - -object AppConfig { - const val APP_NAME = "FIVUCSAS" - const val APP_VERSION = "1.0.0" - - object Api { - const val IDENTITY_BASE_URL = "http://localhost:8080/api/v1" - const val BIOMETRIC_BASE_URL = "http://localhost:8001/api/v1" - const val TIMEOUT_SECONDS = 30L - const val MAX_RETRIES = 3 - } - - object Auth { - const val TOKEN_REFRESH_THRESHOLD_SECONDS = 300L // 5 minutes - const val SESSION_TIMEOUT_MINUTES = 30 - } - - object Biometric { - const val CONFIDENCE_THRESHOLD = 0.85f - const val LIVENESS_THRESHOLD = 0.80f - const val QUALITY_THRESHOLD = 0.75f - const val MAX_ENROLLMENT_RETRIES = 3 - } - - object Cache { - const val MAX_AGE_MINUTES = 15 - const val MAX_SIZE_MB = 50 - } -} -``` - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/config/UIDimens.kt -package com.fivucsas.shared.config - -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -object UIDimens { - // Spacing - val SpacingXSmall: Dp = 4.dp - val SpacingSmall: Dp = 8.dp - val SpacingMedium: Dp = 16.dp - val SpacingLarge: Dp = 24.dp - val SpacingXLarge: Dp = 32.dp - val SpacingXXLarge: Dp = 64.dp - - // Icons - val IconSmall: Dp = 24.dp - val IconMedium: Dp = 32.dp - val IconLarge: Dp = 48.dp - val IconXLarge: Dp = 64.dp - val KioskIconSize: Dp = 120.dp - - // Buttons - val ButtonHeight: Dp = 48.dp - val ButtonHeightKiosk: Dp = 80.dp - val ButtonWidthKiosk: Dp = 250.dp - - // Cards - val CardRadius: Dp = 12.dp - val CardElevation: Dp = 4.dp - - // Camera - val CameraPreviewHeight: Dp = 400.dp - val CameraPreviewWidth: Dp = 600.dp -} -``` - -#### 1.3 Establish Clean Package Structure - -``` -shared/src/commonMain/kotlin/com/fivucsas/shared/ -├── config/ -│ ├── AppConfig.kt -│ ├── UIDimens.kt -│ └── AnimationConfig.kt -├── data/ -│ ├── api/ -│ │ ├── IdentityApiClient.kt -│ │ └── BiometricApiClient.kt -│ ├── repository/ -│ │ ├── AuthRepository.kt -│ │ ├── UserRepository.kt -│ │ └── BiometricRepository.kt -│ └── local/ -│ └── TokenStorage.kt -├── domain/ -│ ├── model/ -│ │ ├── User.kt -│ │ ├── Enrollment.kt -│ │ └── Verification.kt -│ └── usecase/ -│ ├── auth/ -│ │ ├── LoginUseCase.kt -│ │ └── LogoutUseCase.kt -│ └── biometric/ -│ ├── EnrollUserUseCase.kt -│ └── VerifyUserUseCase.kt -├── di/ -│ ├── AppModule.kt -│ ├── NetworkModule.kt -│ └── PlatformModule.kt -├── platform/ -│ ├── camera/ -│ │ └── ICameraService.kt -│ ├── storage/ -│ │ └── ISecureStorage.kt -│ └── logger/ -│ └── ILogger.kt -└── presentation/ - ├── viewmodel/ - │ ├── AdminViewModel.kt - │ ├── KioskViewModel.kt - │ └── AuthViewModel.kt - └── state/ - ├── AdminUiState.kt - ├── KioskUiState.kt - └── AuthUiState.kt -``` - -**Acceptance Criteria**: -- [ ] com.fivucsas.mobile package deleted -- [ ] All imports updated to com.fivucsas.shared -- [ ] Configuration objects created -- [ ] Clean package structure established -- [ ] Build succeeds without errors - ---- - -### Phase 2: Backend Integration - -**Duration**: 4 days -**Priority**: HIGH -**Goal**: Connect to real Identity Core API and Biometric Processor - -#### 2.1 Create Ktor HTTP Client - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/data/api/HttpClientFactory.kt -package com.fivucsas.shared.data.api - -import com.fivucsas.shared.config.AppConfig -import io.ktor.client.* -import io.ktor.client.plugins.* -import io.ktor.client.plugins.auth.* -import io.ktor.client.plugins.auth.providers.* -import io.ktor.client.plugins.contentnegotiation.* -import io.ktor.client.plugins.logging.* -import io.ktor.serialization.kotlinx.json.* -import kotlinx.serialization.json.Json - -class HttpClientFactory( - private val tokenProvider: TokenProvider -) { - fun create(): HttpClient = HttpClient { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - isLenient = true - prettyPrint = false - }) - } - - install(Logging) { - logger = Logger.DEFAULT - level = LogLevel.BODY - } - - install(HttpTimeout) { - requestTimeoutMillis = AppConfig.Api.TIMEOUT_SECONDS * 1000 - connectTimeoutMillis = AppConfig.Api.TIMEOUT_SECONDS * 1000 - } - - install(Auth) { - bearer { - loadTokens { - val accessToken = tokenProvider.getAccessToken() - val refreshToken = tokenProvider.getRefreshToken() - if (accessToken != null && refreshToken != null) { - BearerTokens(accessToken, refreshToken) - } else { - null - } - } - - refreshTokens { - val refreshToken = tokenProvider.getRefreshToken() - if (refreshToken != null) { - val newTokens = tokenProvider.refreshTokens(refreshToken) - BearerTokens(newTokens.accessToken, newTokens.refreshToken) - } else { - null - } - } - } - } - - defaultRequest { - url(AppConfig.Api.IDENTITY_BASE_URL) - } - } -} - -interface TokenProvider { - suspend fun getAccessToken(): String? - suspend fun getRefreshToken(): String? - suspend fun refreshTokens(refreshToken: String): TokenPair - suspend fun clearTokens() -} - -data class TokenPair( - val accessToken: String, - val refreshToken: String -) -``` - -#### 2.2 Create Identity API Client - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/data/api/IdentityApiClient.kt -package com.fivucsas.shared.data.api - -import com.fivucsas.shared.data.api.dto.* -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* - -class IdentityApiClient( - private val httpClient: HttpClient -) { - // Authentication - suspend fun login(request: LoginRequest): Result = runCatching { - httpClient.post("/auth/login") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun refreshToken(request: RefreshRequest): Result = runCatching { - httpClient.post("/auth/refresh") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun logout(): Result = runCatching { - httpClient.post("/auth/logout") - } - - suspend fun getCurrentUser(): Result = runCatching { - httpClient.get("/auth/me").body() - } - - // Users - suspend fun getUsers( - page: Int = 0, - size: Int = 20, - sort: String? = null - ): Result> = runCatching { - httpClient.get("/users") { - parameter("page", page) - parameter("size", size) - sort?.let { parameter("sort", it) } - }.body() - } - - suspend fun getUser(id: Long): Result = runCatching { - httpClient.get("/users/$id").body() - } - - suspend fun createUser(request: CreateUserRequest): Result = runCatching { - httpClient.post("/users") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun updateUser(id: Long, request: UpdateUserRequest): Result = runCatching { - httpClient.put("/users/$id") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun deleteUser(id: Long): Result = runCatching { - httpClient.delete("/users/$id") - } - - // Dashboard - suspend fun getDashboardStatistics(): Result = runCatching { - httpClient.get("/dashboard/statistics").body() - } -} -``` - -#### 2.3 Create Biometric API Client - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/data/api/BiometricApiClient.kt -package com.fivucsas.shared.data.api - -import com.fivucsas.shared.config.AppConfig -import com.fivucsas.shared.data.api.dto.* -import io.ktor.client.* -import io.ktor.client.call.* -import io.ktor.client.request.* -import io.ktor.http.* - -class BiometricApiClient( - private val httpClient: HttpClient -) { - private val baseUrl = AppConfig.Api.BIOMETRIC_BASE_URL - - suspend fun enroll(request: EnrollmentRequest): Result = runCatching { - httpClient.post("$baseUrl/enrollments") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun verify(request: VerificationRequest): Result = runCatching { - httpClient.post("$baseUrl/verify") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun checkLiveness(request: LivenessRequest): Result = runCatching { - httpClient.post("$baseUrl/liveness") { - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } - - suspend fun getEnrollments(userId: Long): Result> = runCatching { - httpClient.get("$baseUrl/enrollments/$userId").body() - } - - suspend fun deleteEnrollment(enrollmentId: String): Result = runCatching { - httpClient.delete("$baseUrl/enrollments/$enrollmentId") - } -} -``` - -#### 2.4 Create Auth Repository - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/data/repository/AuthRepository.kt -package com.fivucsas.shared.data.repository - -import com.fivucsas.shared.data.api.IdentityApiClient -import com.fivucsas.shared.data.api.TokenProvider -import com.fivucsas.shared.data.api.TokenPair -import com.fivucsas.shared.data.api.dto.LoginRequest -import com.fivucsas.shared.data.local.TokenStorage -import com.fivucsas.shared.domain.model.User -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -class AuthRepository( - private val apiClient: IdentityApiClient, - private val tokenStorage: TokenStorage -) : TokenProvider { - - private val _currentUser = MutableStateFlow(null) - val currentUser: StateFlow = _currentUser.asStateFlow() - - private val _isAuthenticated = MutableStateFlow(false) - val isAuthenticated: StateFlow = _isAuthenticated.asStateFlow() - - suspend fun login(email: String, password: String): Result { - val result = apiClient.login(LoginRequest(email, password)) - - return result.fold( - onSuccess = { response -> - tokenStorage.saveTokens( - accessToken = response.accessToken, - refreshToken = response.refreshToken - ) - val user = response.user.toDomain() - _currentUser.value = user - _isAuthenticated.value = true - Result.success(user) - }, - onFailure = { error -> - Result.failure(error) - } - ) - } - - suspend fun logout() { - apiClient.logout() - tokenStorage.clearTokens() - _currentUser.value = null - _isAuthenticated.value = false - } - - suspend fun checkAuthState() { - val token = tokenStorage.getAccessToken() - if (token != null) { - apiClient.getCurrentUser().fold( - onSuccess = { userDto -> - _currentUser.value = userDto.toDomain() - _isAuthenticated.value = true - }, - onFailure = { - tokenStorage.clearTokens() - _isAuthenticated.value = false - } - ) - } - } - - // TokenProvider implementation - override suspend fun getAccessToken(): String? = tokenStorage.getAccessToken() - - override suspend fun getRefreshToken(): String? = tokenStorage.getRefreshToken() - - override suspend fun refreshTokens(refreshToken: String): TokenPair { - val response = apiClient.refreshToken(RefreshRequest(refreshToken)) - .getOrThrow() - - tokenStorage.saveTokens( - accessToken = response.accessToken, - refreshToken = response.refreshToken - ) - - return TokenPair(response.accessToken, response.refreshToken) - } - - override suspend fun clearTokens() { - tokenStorage.clearTokens() - } -} -``` - -#### 2.5 Create Biometric Repository - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/data/repository/BiometricRepository.kt -package com.fivucsas.shared.data.repository - -import com.fivucsas.shared.config.AppConfig -import com.fivucsas.shared.data.api.BiometricApiClient -import com.fivucsas.shared.data.api.dto.* -import com.fivucsas.shared.domain.model.Enrollment -import com.fivucsas.shared.domain.model.VerificationResult -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi - -class BiometricRepository( - private val apiClient: BiometricApiClient -) { - @OptIn(ExperimentalEncodingApi::class) - suspend fun enrollUser( - userId: Long, - imageBytes: ByteArray, - metadata: EnrollmentMetadata? = null - ): Result { - val base64Image = Base64.encode(imageBytes) - - val request = EnrollmentRequest( - userId = userId, - image = base64Image, - metadata = metadata - ) - - return apiClient.enroll(request).map { response -> - Enrollment( - id = response.enrollmentId, - userId = userId, - status = response.status, - qualityScore = response.qualityScore, - message = response.message - ) - } - } - - @OptIn(ExperimentalEncodingApi::class) - suspend fun verifyUser( - imageBytes: ByteArray, - userId: Long? = null, - threshold: Float = AppConfig.Biometric.CONFIDENCE_THRESHOLD - ): Result { - val base64Image = Base64.encode(imageBytes) - - val request = VerificationRequest( - image = base64Image, - userId = userId, - threshold = threshold - ) - - return apiClient.verify(request).map { response -> - VerificationResult( - verified = response.verified, - confidence = response.confidence, - userId = response.userId, - message = response.message, - processingTimeMs = response.processingTimeMs - ) - } - } - - @OptIn(ExperimentalEncodingApi::class) - suspend fun checkLiveness( - imageBytes: ByteArray, - challenge: LivenessChallenge? = null - ): Result { - val base64Image = Base64.encode(imageBytes) - - val request = LivenessRequest( - image = base64Image, - challenge = challenge - ) - - return apiClient.checkLiveness(request).map { response -> - LivenessResult( - isLive = response.isLive, - confidence = response.confidence, - challengePassed = response.challengePassed, - message = response.message - ) - } - } - - suspend fun getEnrollments(userId: Long): Result> { - return apiClient.getEnrollments(userId).map { dtos -> - dtos.map { it.toDomain() } - } - } - - suspend fun deleteEnrollment(enrollmentId: String): Result { - return apiClient.deleteEnrollment(enrollmentId) - } -} - -data class LivenessResult( - val isLive: Boolean, - val confidence: Float, - val challengePassed: Boolean, - val message: String -) -``` - -#### 2.6 Update Koin DI Module - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/di/AppModule.kt -package com.fivucsas.shared.di - -import com.fivucsas.shared.data.api.* -import com.fivucsas.shared.data.local.TokenStorage -import com.fivucsas.shared.data.repository.* -import com.fivucsas.shared.domain.usecase.auth.* -import com.fivucsas.shared.domain.usecase.biometric.* -import com.fivucsas.shared.presentation.viewmodel.* -import org.koin.core.module.dsl.factoryOf -import org.koin.core.module.dsl.singleOf -import org.koin.dsl.module - -val networkModule = module { - single { HttpClientFactory(get()).create() } - singleOf(::IdentityApiClient) - singleOf(::BiometricApiClient) -} - -val repositoryModule = module { - singleOf(::TokenStorage) - singleOf(::AuthRepository) - singleOf(::UserRepository) - singleOf(::BiometricRepository) -} - -val useCaseModule = module { - factoryOf(::LoginUseCase) - factoryOf(::LogoutUseCase) - factoryOf(::GetUsersUseCase) - factoryOf(::EnrollUserUseCase) - factoryOf(::VerifyUserUseCase) - factoryOf(::GetStatisticsUseCase) -} - -val viewModelModule = module { - factoryOf(::AuthViewModel) - factoryOf(::AdminViewModel) - factoryOf(::KioskViewModel) -} - -val appModules = listOf( - networkModule, - repositoryModule, - useCaseModule, - viewModelModule, - platformModule() // Platform-specific bindings -) -``` - -**Acceptance Criteria**: -- [ ] Ktor HTTP client configured with auth -- [ ] Identity API client complete -- [ ] Biometric API client complete -- [ ] Repositories implement domain interfaces -- [ ] DI modules updated -- [ ] Build succeeds without errors - ---- - -### Phase 3: Camera Integration - -**Duration**: 4 days -**Priority**: HIGH -**Goal**: Implement camera capture across all platforms - -#### 3.1 Define Camera Interface - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/platform/camera/ICameraService.kt -package com.fivucsas.shared.platform.camera - -interface ICameraService { - suspend fun initialize(config: CameraConfig): Result - suspend fun capturePhoto(): Result - suspend fun startPreview(onFrame: (ByteArray) -> Unit): Result - suspend fun stopPreview(): Result - suspend fun release(): Result - fun isAvailable(): Boolean -} - -data class CameraConfig( - val resolution: Resolution = Resolution.HD, - val facing: CameraFacing = CameraFacing.FRONT, - val enableFlash: Boolean = false -) - -enum class Resolution(val width: Int, val height: Int) { - VGA(640, 480), - HD(1280, 720), - FULL_HD(1920, 1080) -} - -enum class CameraFacing { - FRONT, BACK -} - -sealed class CameraError : Exception() { - object NotAvailable : CameraError() - object PermissionDenied : CameraError() - object InitializationFailed : CameraError() - object CaptureFailed : CameraError() - data class Unknown(override val message: String?) : CameraError() -} -``` - -#### 3.2 Android Camera Implementation (CameraX) - -```kotlin -// shared/src/androidMain/kotlin/com/fivucsas/shared/platform/camera/AndroidCameraService.kt -package com.fivucsas.shared.platform.camera - -import android.content.Context -import androidx.camera.core.* -import androidx.camera.lifecycle.ProcessCameraProvider -import androidx.core.content.ContextCompat -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.suspendCancellableCoroutine -import kotlinx.coroutines.withContext -import java.util.concurrent.Executors -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException - -class AndroidCameraService( - private val context: Context -) : ICameraService { - - private var cameraProvider: ProcessCameraProvider? = null - private var imageCapture: ImageCapture? = null - private var imageAnalysis: ImageAnalysis? = null - private val executor = Executors.newSingleThreadExecutor() - - override suspend fun initialize(config: CameraConfig): Result = withContext(Dispatchers.Main) { - try { - val provider = suspendCancellableCoroutine { cont -> - ProcessCameraProvider.getInstance(context).apply { - addListener({ - cont.resume(get()) - }, ContextCompat.getMainExecutor(context)) - } - } - - cameraProvider = provider - - val cameraSelector = when (config.facing) { - CameraFacing.FRONT -> CameraSelector.DEFAULT_FRONT_CAMERA - CameraFacing.BACK -> CameraSelector.DEFAULT_BACK_CAMERA - } - - val targetResolution = android.util.Size( - config.resolution.width, - config.resolution.height - ) - - imageCapture = ImageCapture.Builder() - .setTargetResolution(targetResolution) - .setCaptureMode(ImageCapture.CAPTURE_MODE_MAXIMIZE_QUALITY) - .build() - - imageAnalysis = ImageAnalysis.Builder() - .setTargetResolution(targetResolution) - .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) - .build() - - Result.success(Unit) - } catch (e: Exception) { - Result.failure(CameraError.InitializationFailed) - } - } - - override suspend fun capturePhoto(): Result = withContext(Dispatchers.IO) { - val capture = imageCapture ?: return@withContext Result.failure(CameraError.NotAvailable) - - try { - val bytes = suspendCancellableCoroutine { cont -> - capture.takePicture(executor, object : ImageCapture.OnImageCapturedCallback() { - override fun onCaptureSuccess(image: ImageProxy) { - val buffer = image.planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) - image.close() - cont.resume(bytes) - } - - override fun onError(exception: ImageCaptureException) { - cont.resumeWithException(exception) - } - }) - } - Result.success(bytes) - } catch (e: Exception) { - Result.failure(CameraError.CaptureFailed) - } - } - - override suspend fun startPreview(onFrame: (ByteArray) -> Unit): Result { - imageAnalysis?.setAnalyzer(executor) { imageProxy -> - val buffer = imageProxy.planes[0].buffer - val bytes = ByteArray(buffer.remaining()) - buffer.get(bytes) - onFrame(bytes) - imageProxy.close() - } - return Result.success(Unit) - } - - override suspend fun stopPreview(): Result { - imageAnalysis?.clearAnalyzer() - return Result.success(Unit) - } - - override suspend fun release(): Result { - cameraProvider?.unbindAll() - cameraProvider = null - imageCapture = null - imageAnalysis = null - return Result.success(Unit) - } - - override fun isAvailable(): Boolean { - return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) == true - } -} -``` - -#### 3.3 Desktop Camera Implementation (JavaCV) - -```kotlin -// shared/src/desktopMain/kotlin/com/fivucsas/shared/platform/camera/DesktopCameraService.kt -package com.fivucsas.shared.platform.camera - -import kotlinx.coroutines.* -import org.bytedeco.javacv.FrameGrabber -import org.bytedeco.javacv.Java2DFrameConverter -import org.bytedeco.javacv.OpenCVFrameGrabber -import java.awt.image.BufferedImage -import java.io.ByteArrayOutputStream -import javax.imageio.ImageIO - -class DesktopCameraService : ICameraService { - - private var grabber: FrameGrabber? = null - private var converter: Java2DFrameConverter? = null - private var previewJob: Job? = null - private var isInitialized = false - - override suspend fun initialize(config: CameraConfig): Result = withContext(Dispatchers.IO) { - try { - grabber = OpenCVFrameGrabber(0).apply { - imageWidth = config.resolution.width - imageHeight = config.resolution.height - start() - } - converter = Java2DFrameConverter() - isInitialized = true - Result.success(Unit) - } catch (e: Exception) { - Result.failure(CameraError.InitializationFailed) - } - } - - override suspend fun capturePhoto(): Result = withContext(Dispatchers.IO) { - if (!isInitialized) { - return@withContext Result.failure(CameraError.NotAvailable) - } - - try { - val frame = grabber?.grab() - ?: return@withContext Result.failure(CameraError.CaptureFailed) - - val image = converter?.convert(frame) - ?: return@withContext Result.failure(CameraError.CaptureFailed) - - val outputStream = ByteArrayOutputStream() - ImageIO.write(image, "jpg", outputStream) - Result.success(outputStream.toByteArray()) - } catch (e: Exception) { - Result.failure(CameraError.CaptureFailed) - } - } - - override suspend fun startPreview(onFrame: (ByteArray) -> Unit): Result { - if (!isInitialized) { - return Result.failure(CameraError.NotAvailable) - } - - previewJob = CoroutineScope(Dispatchers.IO).launch { - while (isActive) { - try { - val frame = grabber?.grab() ?: continue - val image = converter?.convert(frame) ?: continue - - val outputStream = ByteArrayOutputStream() - ImageIO.write(image, "jpg", outputStream) - onFrame(outputStream.toByteArray()) - - delay(33) // ~30 FPS - } catch (e: Exception) { - // Log error and continue - } - } - } - - return Result.success(Unit) - } - - override suspend fun stopPreview(): Result { - previewJob?.cancel() - previewJob = null - return Result.success(Unit) - } - - override suspend fun release(): Result = withContext(Dispatchers.IO) { - try { - stopPreview() - grabber?.stop() - grabber?.release() - grabber = null - converter = null - isInitialized = false - Result.success(Unit) - } catch (e: Exception) { - Result.failure(CameraError.Unknown(e.message)) - } - } - - override fun isAvailable(): Boolean { - return try { - val devices = OpenCVFrameGrabber.getDeviceDescriptions() - devices.isNotEmpty() - } catch (e: Exception) { - false - } - } -} -``` - -#### 3.4 iOS Camera Stub (expect/actual) - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/platform/camera/CameraServiceFactory.kt -package com.fivucsas.shared.platform.camera - -expect fun createCameraService(): ICameraService - -// shared/src/androidMain/kotlin/.../CameraServiceFactory.kt -actual fun createCameraService(): ICameraService { - return AndroidCameraService(applicationContext) -} - -// shared/src/desktopMain/kotlin/.../CameraServiceFactory.kt -actual fun createCameraService(): ICameraService { - return DesktopCameraService() -} - -// shared/src/iosMain/kotlin/.../CameraServiceFactory.kt -actual fun createCameraService(): ICameraService { - return IosCameraService() -} -``` - -**Acceptance Criteria**: -- [ ] Camera interface defined -- [ ] Android CameraX implementation complete -- [ ] Desktop JavaCV implementation complete -- [ ] iOS stub created (full implementation optional) -- [ ] Camera service registered in DI -- [ ] Photo capture works on all platforms - ---- - -### Phase 4: Biometric Flow Implementation - -**Duration**: 4 days -**Priority**: HIGH -**Goal**: Complete enrollment and verification flows - -#### 4.1 Update Kiosk ViewModel - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/presentation/viewmodel/KioskViewModel.kt -package com.fivucsas.shared.presentation.viewmodel - -import com.fivucsas.shared.config.AppConfig -import com.fivucsas.shared.data.repository.BiometricRepository -import com.fivucsas.shared.data.repository.UserRepository -import com.fivucsas.shared.platform.camera.CameraConfig -import com.fivucsas.shared.platform.camera.ICameraService -import com.fivucsas.shared.presentation.state.KioskUiState -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch - -class KioskViewModel( - private val biometricRepository: BiometricRepository, - private val userRepository: UserRepository, - private val cameraService: ICameraService -) { - private val scope = CoroutineScope(Dispatchers.Main) - - private val _uiState = MutableStateFlow(KioskUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - fun initializeCamera() { - scope.launch { - _uiState.update { it.copy(cameraState = CameraState.Initializing) } - - cameraService.initialize(CameraConfig(facing = CameraFacing.FRONT)) - .fold( - onSuccess = { - _uiState.update { it.copy(cameraState = CameraState.Ready) } - }, - onFailure = { error -> - _uiState.update { - it.copy( - cameraState = CameraState.Error, - errorMessage = error.message - ) - } - } - ) - } - } - - fun startEnrollment( - firstName: String, - lastName: String, - email: String - ) { - scope.launch { - _uiState.update { it.copy(isProcessing = true, errorMessage = null) } - - // First create user - val userResult = userRepository.createUser( - firstName = firstName, - lastName = lastName, - email = email - ) - - userResult.fold( - onSuccess = { user -> - _uiState.update { - it.copy( - currentUserId = user.id, - enrollmentStep = EnrollmentStep.CAPTURE - ) - } - }, - onFailure = { error -> - _uiState.update { - it.copy( - isProcessing = false, - errorMessage = "Failed to create user: ${error.message}" - ) - } - } - ) - } - } - - fun captureAndEnroll() { - scope.launch { - val userId = _uiState.value.currentUserId - ?: return@launch setError("No user selected") - - _uiState.update { it.copy(enrollmentStep = EnrollmentStep.CAPTURING) } - - // Step 1: Capture photo - val captureResult = cameraService.capturePhoto() - val imageBytes = captureResult.getOrElse { - return@launch setError("Failed to capture photo") - } - - // Step 2: Check liveness - _uiState.update { it.copy(enrollmentStep = EnrollmentStep.LIVENESS_CHECK) } - - val livenessResult = biometricRepository.checkLiveness(imageBytes) - val liveness = livenessResult.getOrElse { - return@launch setError("Liveness check failed") - } - - if (!liveness.isLive || liveness.confidence < AppConfig.Biometric.LIVENESS_THRESHOLD) { - return@launch setError("Liveness check failed. Please try again.") - } - - // Step 3: Enroll user - _uiState.update { it.copy(enrollmentStep = EnrollmentStep.ENROLLING) } - - val enrollResult = biometricRepository.enrollUser(userId, imageBytes) - - enrollResult.fold( - onSuccess = { enrollment -> - if (enrollment.qualityScore >= AppConfig.Biometric.QUALITY_THRESHOLD) { - _uiState.update { - it.copy( - enrollmentStep = EnrollmentStep.SUCCESS, - isProcessing = false, - successMessage = "Enrollment successful!" - ) - } - } else { - setError("Image quality too low. Please try again with better lighting.") - } - }, - onFailure = { error -> - setError("Enrollment failed: ${error.message}") - } - ) - } - } - - fun startVerification() { - scope.launch { - _uiState.update { - it.copy( - verificationStep = VerificationStep.READY, - errorMessage = null, - successMessage = null - ) - } - } - } - - fun captureAndVerify() { - scope.launch { - _uiState.update { it.copy(verificationStep = VerificationStep.CAPTURING) } - - // Step 1: Capture photo - val captureResult = cameraService.capturePhoto() - val imageBytes = captureResult.getOrElse { - return@launch setVerificationError("Failed to capture photo") - } - - // Step 2: Check liveness - _uiState.update { it.copy(verificationStep = VerificationStep.LIVENESS_CHECK) } - - val livenessResult = biometricRepository.checkLiveness(imageBytes) - val liveness = livenessResult.getOrElse { - return@launch setVerificationError("Liveness check failed") - } - - if (!liveness.isLive) { - return@launch setVerificationError("Liveness check failed. Please try again.") - } - - // Step 3: Verify - _uiState.update { it.copy(verificationStep = VerificationStep.VERIFYING) } - - val verifyResult = biometricRepository.verifyUser(imageBytes) - - verifyResult.fold( - onSuccess = { result -> - if (result.verified && result.confidence >= AppConfig.Biometric.CONFIDENCE_THRESHOLD) { - _uiState.update { - it.copy( - verificationStep = VerificationStep.SUCCESS, - verifiedUserId = result.userId, - verificationConfidence = result.confidence, - successMessage = "Verification successful!" - ) - } - } else { - setVerificationError( - "Verification failed. Confidence: ${(result.confidence * 100).toInt()}%" - ) - } - }, - onFailure = { error -> - setVerificationError("Verification failed: ${error.message}") - } - ) - } - } - - fun resetToWelcome() { - _uiState.update { KioskUiState() } - } - - fun releaseCamera() { - scope.launch { - cameraService.release() - } - } - - private fun setError(message: String) { - _uiState.update { - it.copy( - isProcessing = false, - errorMessage = message, - enrollmentStep = EnrollmentStep.ERROR - ) - } - } - - private fun setVerificationError(message: String) { - _uiState.update { - it.copy( - verificationStep = VerificationStep.FAILED, - errorMessage = message - ) - } - } -} -``` - -#### 4.2 Define UI State - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/presentation/state/KioskUiState.kt -package com.fivucsas.shared.presentation.state - -data class KioskUiState( - val screen: KioskScreen = KioskScreen.WELCOME, - val cameraState: CameraState = CameraState.NotInitialized, - val isProcessing: Boolean = false, - val errorMessage: String? = null, - val successMessage: String? = null, - - // Enrollment - val currentUserId: Long? = null, - val enrollmentStep: EnrollmentStep = EnrollmentStep.FORM, - - // Verification - val verificationStep: VerificationStep = VerificationStep.READY, - val verifiedUserId: Long? = null, - val verificationConfidence: Float? = null -) - -enum class KioskScreen { - WELCOME, - ENROLLMENT, - VERIFICATION -} - -enum class CameraState { - NotInitialized, - Initializing, - Ready, - Error -} - -enum class EnrollmentStep { - FORM, - CAPTURE, - CAPTURING, - LIVENESS_CHECK, - ENROLLING, - SUCCESS, - ERROR -} - -enum class VerificationStep { - READY, - CAPTURING, - LIVENESS_CHECK, - VERIFYING, - SUCCESS, - FAILED -} -``` - -**Acceptance Criteria**: -- [ ] Enrollment flow complete with liveness check -- [ ] Verification flow complete with liveness check -- [ ] Camera integration working -- [ ] Error states properly handled -- [ ] Success/failure feedback displayed -- [ ] Retry logic implemented - ---- - -### Phase 5: Platform-Specific Features - -**Duration**: 3 days -**Priority**: MEDIUM -**Goal**: Implement platform-specific optimizations - -#### 5.1 Android-Specific Features - -```kotlin -// androidApp/src/main/kotlin/com/fivucsas/android/MainActivity.kt -package com.fivucsas.android - -import android.Manifest -import android.content.pm.PackageManager -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.runtime.* -import androidx.core.content.ContextCompat -import com.fivucsas.shared.ui.theme.AppTheme - -class MainActivity : ComponentActivity() { - - private val cameraPermissionLauncher = registerForActivityResult( - ActivityResultContracts.RequestPermission() - ) { isGranted -> - if (isGranted) { - // Camera permission granted - } else { - // Show permission denied message - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - checkCameraPermission() - - setContent { - AppTheme { - App() - } - } - } - - private fun checkCameraPermission() { - when { - ContextCompat.checkSelfPermission( - this, - Manifest.permission.CAMERA - ) == PackageManager.PERMISSION_GRANTED -> { - // Permission already granted - } - else -> { - cameraPermissionLauncher.launch(Manifest.permission.CAMERA) - } - } - } -} -``` - -#### 5.2 Secure Storage Implementation - -```kotlin -// shared/src/androidMain/kotlin/.../storage/AndroidSecureStorage.kt -package com.fivucsas.shared.platform.storage - -import android.content.Context -import androidx.security.crypto.EncryptedSharedPreferences -import androidx.security.crypto.MasterKey - -class AndroidSecureStorage(context: Context) : ISecureStorage { - - private val masterKey = MasterKey.Builder(context) - .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) - .build() - - private val sharedPreferences = EncryptedSharedPreferences.create( - context, - "fivucsas_secure_prefs", - masterKey, - EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, - EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM - ) - - override suspend fun saveString(key: String, value: String) { - sharedPreferences.edit().putString(key, value).apply() - } - - override suspend fun getString(key: String): String? { - return sharedPreferences.getString(key, null) - } - - override suspend fun delete(key: String) { - sharedPreferences.edit().remove(key).apply() - } - - override suspend fun clear() { - sharedPreferences.edit().clear().apply() - } -} - -// shared/src/desktopMain/kotlin/.../storage/DesktopSecureStorage.kt -class DesktopSecureStorage : ISecureStorage { - private val preferences = java.util.prefs.Preferences.userNodeForPackage( - DesktopSecureStorage::class.java - ) - - override suspend fun saveString(key: String, value: String) { - preferences.put(key, value) - } - - override suspend fun getString(key: String): String? { - return preferences.get(key, null) - } - - override suspend fun delete(key: String) { - preferences.remove(key) - } - - override suspend fun clear() { - preferences.clear() - } -} -``` - -**Acceptance Criteria**: -- [ ] Android permissions handled properly -- [ ] Secure storage working on Android -- [ ] Secure storage working on Desktop -- [ ] Platform-specific DI bindings -- [ ] No platform-specific code in shared module - ---- - -### Phase 6: Test Coverage Expansion - -**Duration**: 3 days -**Priority**: HIGH -**Target**: 70% code coverage - -#### 6.1 ViewModel Tests - -```kotlin -// shared/src/commonTest/kotlin/.../viewmodel/KioskViewModelTest.kt -package com.fivucsas.shared.presentation.viewmodel - -import app.cash.turbine.test -import com.fivucsas.shared.data.repository.BiometricRepository -import com.fivucsas.shared.data.repository.UserRepository -import com.fivucsas.shared.platform.camera.ICameraService -import com.fivucsas.shared.presentation.state.* -import io.mockk.* -import kotlinx.coroutines.test.runTest -import kotlin.test.* - -class KioskViewModelTest { - - private lateinit var viewModel: KioskViewModel - private lateinit var biometricRepository: BiometricRepository - private lateinit var userRepository: UserRepository - private lateinit var cameraService: ICameraService - - @BeforeTest - fun setup() { - biometricRepository = mockk() - userRepository = mockk() - cameraService = mockk() - - viewModel = KioskViewModel( - biometricRepository = biometricRepository, - userRepository = userRepository, - cameraService = cameraService - ) - } - - @Test - fun `initializeCamera should update state to Ready on success`() = runTest { - // Given - coEvery { cameraService.initialize(any()) } returns Result.success(Unit) - - // When - viewModel.initializeCamera() - - // Then - viewModel.uiState.test { - val initialState = awaitItem() - assertEquals(CameraState.NotInitialized, initialState.cameraState) - - val initializingState = awaitItem() - assertEquals(CameraState.Initializing, initializingState.cameraState) - - val readyState = awaitItem() - assertEquals(CameraState.Ready, readyState.cameraState) - } - } - - @Test - fun `captureAndEnroll should fail if liveness check fails`() = runTest { - // Given - coEvery { cameraService.capturePhoto() } returns Result.success(byteArrayOf()) - coEvery { biometricRepository.checkLiveness(any()) } returns Result.success( - LivenessResult(isLive = false, confidence = 0.3f, challengePassed = false, message = "Failed") - ) - - viewModel._uiState.value = viewModel.uiState.value.copy(currentUserId = 1L) - - // When - viewModel.captureAndEnroll() - - // Then - viewModel.uiState.test { - val state = awaitItem() - assertEquals(EnrollmentStep.ERROR, state.enrollmentStep) - assertNotNull(state.errorMessage) - } - } - - @Test - fun `captureAndVerify should succeed with valid face`() = runTest { - // Given - coEvery { cameraService.capturePhoto() } returns Result.success(byteArrayOf()) - coEvery { biometricRepository.checkLiveness(any()) } returns Result.success( - LivenessResult(isLive = true, confidence = 0.95f, challengePassed = true, message = "OK") - ) - coEvery { biometricRepository.verifyUser(any()) } returns Result.success( - VerificationResult(verified = true, confidence = 0.92f, userId = 1L, message = "Match", processingTimeMs = 150) - ) - - // When - viewModel.captureAndVerify() - - // Then - viewModel.uiState.test { - skipItems(3) // Skip intermediate states - val state = awaitItem() - assertEquals(VerificationStep.SUCCESS, state.verificationStep) - assertEquals(1L, state.verifiedUserId) - } - } - - // Add more tests... -} -``` - -#### 6.2 Repository Tests - -```kotlin -// shared/src/commonTest/kotlin/.../repository/BiometricRepositoryTest.kt -package com.fivucsas.shared.data.repository - -import com.fivucsas.shared.data.api.BiometricApiClient -import com.fivucsas.shared.data.api.dto.* -import io.mockk.* -import kotlinx.coroutines.test.runTest -import kotlin.test.* - -class BiometricRepositoryTest { - - private lateinit var repository: BiometricRepository - private lateinit var apiClient: BiometricApiClient - - @BeforeTest - fun setup() { - apiClient = mockk() - repository = BiometricRepository(apiClient) - } - - @Test - fun `enrollUser should return enrollment on success`() = runTest { - // Given - val userId = 1L - val imageBytes = byteArrayOf(1, 2, 3) - - coEvery { apiClient.enroll(any()) } returns Result.success( - EnrollmentResponse( - enrollmentId = "enroll-123", - status = EnrollmentStatus.COMPLETED, - qualityScore = 0.92f, - message = "Success" - ) - ) - - // When - val result = repository.enrollUser(userId, imageBytes) - - // Then - assertTrue(result.isSuccess) - val enrollment = result.getOrThrow() - assertEquals("enroll-123", enrollment.id) - assertEquals(0.92f, enrollment.qualityScore) - } - - @Test - fun `verifyUser should return verification result`() = runTest { - // Given - val imageBytes = byteArrayOf(1, 2, 3) - - coEvery { apiClient.verify(any()) } returns Result.success( - VerificationResponse( - verified = true, - confidence = 0.95f, - userId = 1L, - message = "Match found", - processingTimeMs = 150 - ) - ) - - // When - val result = repository.verifyUser(imageBytes) - - // Then - assertTrue(result.isSuccess) - val verification = result.getOrThrow() - assertTrue(verification.verified) - assertEquals(0.95f, verification.confidence) - } -} -``` - -**Acceptance Criteria**: -- [ ] ViewModel tests: 80% coverage -- [ ] Repository tests: 90% coverage -- [ ] Use case tests: 90% coverage -- [ ] All tests passing -- [ ] Coverage reports generated - ---- - -### Phase 7: Production Readiness - -**Duration**: 1 day -**Priority**: HIGH -**Goal**: Final polish and production configuration - -#### 7.1 Environment Configuration - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/config/Environment.kt -package com.fivucsas.shared.config - -enum class Environment { - DEVELOPMENT, - STAGING, - PRODUCTION -} - -object EnvironmentConfig { - var current: Environment = Environment.DEVELOPMENT - private set - - fun configure(environment: Environment) { - current = environment - } - - val identityBaseUrl: String - get() = when (current) { - Environment.DEVELOPMENT -> "http://localhost:8080/api/v1" - Environment.STAGING -> "https://staging-api.fivucsas.com/api/v1" - Environment.PRODUCTION -> "https://api.fivucsas.com/api/v1" - } - - val biometricBaseUrl: String - get() = when (current) { - Environment.DEVELOPMENT -> "http://localhost:8001/api/v1" - Environment.STAGING -> "https://staging-biometric.fivucsas.com/api/v1" - Environment.PRODUCTION -> "https://biometric.fivucsas.com/api/v1" - } - - val enableLogging: Boolean - get() = current != Environment.PRODUCTION - - val useMockApi: Boolean - get() = false // Set to false for production -} -``` - -#### 7.2 Error Handling - -```kotlin -// shared/src/commonMain/kotlin/com/fivucsas/shared/domain/error/AppError.kt -package com.fivucsas.shared.domain.error - -sealed class AppError : Exception() { - data class Network(override val message: String?) : AppError() - data class Authentication(override val message: String?) : AppError() - data class Authorization(override val message: String?) : AppError() - data class Validation(override val message: String?, val fields: Map? = null) : AppError() - data class NotFound(override val message: String?) : AppError() - data class Server(override val message: String?) : AppError() - data class Unknown(override val message: String?) : AppError() -} - -fun Throwable.toAppError(): AppError { - return when (this) { - is AppError -> this - is java.net.UnknownHostException -> AppError.Network("No internet connection") - is java.net.SocketTimeoutException -> AppError.Network("Connection timeout") - else -> AppError.Unknown(message) - } -} -``` - -**Acceptance Criteria**: -- [ ] Environment configuration complete -- [ ] Error handling comprehensive -- [ ] Logging configured per environment -- [ ] Release builds tested -- [ ] No hardcoded development URLs - ---- - -## Integration Points - -### With Identity Core API - -``` -Client Apps Identity Core API - │ │ - ├── POST /auth/login ─────────►│ - │◄──── JWT tokens ─────────────┤ - │ │ - ├── GET /users ───────────────►│ - │ (Bearer token) │ - │◄──── Paginated users ────────┤ - │ │ - ├── POST /users ──────────────►│ - │◄──── Created user ───────────┤ -``` - -### With Biometric Processor - -``` -Client Apps Biometric Processor - │ │ - ├── POST /enrollments ────────►│ - │ (Base64 image) │ - │◄──── Enrollment result ──────┤ - │ │ - ├── POST /verify ─────────────►│ - │ (Base64 image) │ - │◄──── Verification result ────┤ - │ │ - ├── POST /liveness ───────────►│ - │ (Base64 image) │ - │◄──── Liveness result ────────┤ -``` - ---- - -## Testing Strategy - -### Test Types - -| Type | Coverage Target | Tools | -|------|-----------------|-------| -| Unit Tests | ≥80% | kotlin.test + MockK | -| Integration Tests | Critical paths | Ktor MockEngine | -| UI Tests | Happy paths | Compose Testing | - -### Test Commands - -```bash -# Run all tests -./gradlew test - -# Run with coverage -./gradlew koverReport - -# Run desktop app -./gradlew desktopApp:run - -# Run Android app (requires emulator/device) -./gradlew androidApp:installDebug -``` - ---- - -## Deployment Checklist - -### Pre-Release - -- [ ] All tests pass -- [ ] Build succeeds on all platforms -- [ ] Mock API disabled -- [ ] Environment configured for production -- [ ] Camera permissions handled -- [ ] Error handling tested - -### Build Commands - -```bash -# Desktop -./gradlew desktopApp:packageDistributionForCurrentOS - -# Android -./gradlew androidApp:assembleRelease - -# iOS (requires macOS) -./gradlew iosApp:linkReleaseFrameworkIosArm64 -``` - ---- - -## Timeline Summary - -| Phase | Duration | Dependencies | -|-------|----------|--------------| -| Phase 1: Package Cleanup | 2 days | None | -| Phase 2: Backend Integration | 4 days | Identity API, Biometric API | -| Phase 3: Camera Integration | 4 days | Platform SDKs | -| Phase 4: Biometric Flow | 4 days | Phases 2, 3 | -| Phase 5: Platform Features | 3 days | Phases 1-4 | -| Phase 6: Test Coverage | 3 days | All features | -| Phase 7: Production Ready | 1 day | All phases | -| **Total** | **21 days** | | - ---- - -## Success Criteria - -| Metric | Target | -|--------|--------| -| Feature Completion | 100% | -| Test Coverage | ≥70% | -| Build Time | <5 min | -| Android APK Size | <50MB | -| Desktop App Size | <100MB | - ---- - -**Document Status**: Ready for Implementation -**Last Updated**: January 2026 -**Next Review**: After Phase 7 completion diff --git a/docs/archive/2026-04-16/client-apps-MODULE_PLAN.md b/docs/archive/2026-04-16/client-apps-MODULE_PLAN.md deleted file mode 100644 index 378693b1..00000000 --- a/docs/archive/2026-04-16/client-apps-MODULE_PLAN.md +++ /dev/null @@ -1,989 +0,0 @@ -# Mobile App - Module Implementation Plan - -**Module Name**: mobile-app (includes Desktop + Mobile) -**Repository**: https://github.com/Rollingcat-Software/mobile-app -**Technology**: Kotlin Multiplatform + Compose Multiplatform -**Purpose**: Cross-platform kiosk mode and admin dashboard for Desktop, Android, and iOS -**Status**: ⚠️ Desktop 96% Complete (UI), Mobile Not Started -**Priority**: 🟡 MEDIUM - Can develop after backend integration complete - ---- - -## 📋 Table of Contents - -1. [Module Overview](#module-overview) -2. [Current Status](#current-status) -3. [Architecture](#architecture) -4. [Project Structure](#project-structure) -5. [Implementation Tasks](#implementation-tasks) -6. [Testing Requirements](#testing-requirements) -7. [Deployment](#deployment) -8. [Integration Points](#integration-points) - ---- - -## 🎯 Module Overview - -### Purpose - -The mobile-app repository contains **both desktop and mobile applications** using Kotlin -Multiplatform. It provides: - -**Desktop App** (Kiosk Mode + Admin Dashboard): - -- Kiosk mode for enrollment and verification -- Admin dashboard for managing users and viewing analytics -- Runs on Windows, macOS, Linux - -**Mobile App** (Planned - Android/iOS): - -- Mobile enrollment and verification -- User profile management -- Biometric authentication on mobile devices - -### Key Features - -1. **Kiosk Mode** (Desktop) - - Welcome screen with enrollment/verification options - - Enrollment flow with camera capture - - Verification flow with liveness detection - - Success/failure feedback - -2. **Admin Dashboard** (Desktop) - - Users tab - User management with CRUD operations - - Analytics tab - Charts and statistics - - Security tab - Audit logs and security events - - Settings tab - System and profile configuration - -3. **Shared Code** (90%) - - Business logic shared between desktop and mobile - - Network layer (Ktor client) - - Data models - - ViewModels - ---- - -## 📊 Current Status - -### ✅ Desktop App: What's Implemented (96% Complete) - -#### Kiosk Mode (100% Complete) - -**File**: `desktopApp/src/jvmMain/kotlin/KioskMode.kt` - -- ✅ **Welcome Screen** - - Gradient background - - Enrollment and verification buttons - - Employee ID input field - - Modern UI with shadows and gradients - -- ✅ **Enrollment Screen** - - User information form (name, email, employee ID) - - Camera button for photo capture - - Form validation - - Submit button with loading state - - Success/error messages - -- ✅ **Verification Screen** - - Camera button for face capture - - Success state: Green gradient icon, confidence score, progress bar - - Failure state: Red gradient icon, retry/cancel buttons - - Loading state with circular progress indicator - - Liveness detection UI - -#### Admin Dashboard (96% Complete) - -**File**: `desktopApp/src/jvmMain/kotlin/AdminDashboard.kt` (2211 lines) - -- ✅ **Users Tab** (100% Complete) - - Statistics cards (Total, Active, Inactive, Pending) - - User list table with search - - Add/Edit/Delete dialogs - - Pagination controls - - Status badges - - Export functionality - - Mock data working - -- ✅ **Analytics Tab** (100% Complete) - - Statistics cards overview - - Verification trends chart placeholder - - Success rate chart placeholder - - Recent verifications list - - Mock data working - -- ✅ **Security Tab** (80% Complete) - - 3 security alert cards (Failed Logins, Active Sessions, Suspicious Activity) - - Recent activity table with expandable details - - ❌ Missing: Detailed audit logs table - - ❌ Missing: Advanced filtering - -- ✅ **Settings Tab** (100% Complete - Nov 17, 2025) - - 6 comprehensive sections: - 1. Profile (avatar, name, email, role) - 2. Security (password, 2FA, session timeout) - 3. Biometric (face match threshold, liveness, quality) - 4. System (API URL, logging, cache, performance) - 5. Notifications (email alerts, reports, updates) - 6. Appearance (theme, display, date format, timezone) - - 25+ input controls (text fields, sliders, switches, dropdowns) - - Settings navigation panel - - Save/Cancel buttons - -### ❌ Mobile App: Not Started - -The mobile app (Android/iOS) has not been implemented yet. The shared code (93 Kotlin files) -provides foundation, but platform-specific UI and features are missing. - -### ⚠️ Backend Integration: Not Started - -All desktop functionality uses mock data. No backend API integration exists. - ---- - -## 🏗️ Architecture - -### Technology Stack - -```yaml -Language: Kotlin 1.9+ -Framework: Kotlin Multiplatform (KMP) -UI: Compose Multiplatform -Desktop: JVM (Windows, macOS, Linux) -Mobile: Android (Kotlin), iOS (Swift interop via KMP) -Networking: Ktor Client -Serialization: Kotlinx Serialization -State Management: ViewModel + StateFlow -Build: Gradle 8+ -``` - -### Project Structure (Kotlin Multiplatform) - -``` -mobile-app/ -├── shared/ # 90% shared code -│ ├── src/ -│ │ ├── commonMain/ # Shared across all platforms -│ │ │ ├── kotlin/ -│ │ │ │ ├── models/ # Data models (User, Tenant, etc.) -│ │ │ │ ├── network/ # API client (Ktor) -│ │ │ │ ├── viewmodels/# Business logic -│ │ │ │ └── repository/# Data layer -│ │ ├── androidMain/ # Android-specific code -│ │ ├── iosMain/ # iOS-specific code -│ │ └── jvmMain/ # Desktop-specific code -│ -├── desktopApp/ # Desktop application -│ └── src/jvmMain/kotlin/ -│ ├── Main.kt -│ ├── KioskMode.kt # Kiosk UI (2000+ lines) -│ └── AdminDashboard.kt # Admin UI (2211 lines) -│ -├── androidApp/ # Android application (to be created) -│ └── src/main/ -│ ├── kotlin/ -│ └── res/ -│ -├── iosApp/ # iOS application (to be created) -│ └── iosApp/ -│ └── ContentView.swift -│ -└── build.gradle.kts -``` - -### Code Sharing - -``` -┌────────────────────────────────────────┐ -│ Shared Module (90%) │ -│ - Models │ -│ - ViewModels │ -│ - Network (Ktor) │ -│ - Business Logic │ -└───────────┬────────────────────────────┘ - │ - ┌───────┴────────┬────────────┐ - │ │ │ -┌───▼────┐ ┌──────▼──┐ ┌─────▼─────┐ -│Desktop │ │ Android │ │ iOS │ -│(Compose│ │(Compose)│ │ (SwiftUI)│ -│ JVM) │ │ │ │ │ -└────────┘ └─────────┘ └───────────┘ -``` - ---- - -## 🧩 Component Structure - -### Desktop App Components - -#### KioskMode.kt (Enrollment & Verification) - -```kotlin -@Composable -fun KioskMode() { - // Screens: - // 1. WelcomeScreen() - Entry point - // 2. EnrollmentScreen() - User enrollment - // 3. VerificationScreen() - Face verification - - // State management with mutableStateOf - // Navigation between screens - // Camera integration (placeholder) - // API calls (mock mode) -} -``` - -#### AdminDashboard.kt (Admin Interface) - -```kotlin -@Composable -fun AdminDashboard() { - // Tabs: - // 1. UsersTab() - User CRUD - // 2. AnalyticsTab() - Charts and stats - // 3. SecurityTab() - Audit logs - // 4. SettingsTab() - Configuration - - // Components: - // - TopBar with app name and user menu - // - Sidebar navigation - // - Content area for selected tab - // - Dialogs for create/edit -} -``` - -### Shared Code Structure - -#### Models (commonMain) - -```kotlin -// User.kt -data class User( - val id: Long, - val email: String, - val firstName: String, - val lastName: String, - val role: String, - val status: String, - val tenantId: Long, - val createdAt: String -) - -// Tenant.kt, BiometricData.kt, etc. -``` - -#### API Client (commonMain) - -```kotlin -// ApiClient.kt -class ApiClient { - private val client = HttpClient { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - isLenient = true - }) - } - } - - suspend fun login(email: String, password: String): AuthResponse - suspend fun getUsers(): List - suspend fun createUser(user: CreateUserRequest): User - // etc. -} -``` - -#### ViewModels (commonMain) - -```kotlin -// UsersViewModel.kt -class UsersViewModel { - private val _users = MutableStateFlow>(emptyList()) - val users: StateFlow> = _users - - suspend fun fetchUsers() - suspend fun createUser(user: User) - suspend fun updateUser(id: Long, user: User) - suspend fun deleteUser(id: Long) -} -``` - ---- - -## 📝 Implementation Tasks - -### Phase 1: Desktop Backend Integration (3-4 hours) - -**Priority**: 🔴 CRITICAL - -#### Task 1.1: Create API Service Layer - -- [ ] Create `ApiService.kt` in shared/commonMain -- [ ] Configure Ktor client with base URL -- [ ] Add JWT token interceptor -- [ ] Implement authentication endpoints (login, logout) -- [ ] Implement user CRUD endpoints - -```kotlin -// shared/src/commonMain/kotlin/network/ApiService.kt -class ApiService(private val baseUrl: String) { - private val client = HttpClient { - install(ContentNegotiation) { json() } - install(Auth) { - bearer { - loadTokens { /* Load from storage */ } - } - } - } - - suspend fun login(email: String, password: String): AuthResponse { - return client.post("$baseUrl/auth/login") { - contentType(ContentType.Application.Json) - setBody(LoginRequest(email, password)) - }.body() - } - - suspend fun getUsers(): List { - return client.get("$baseUrl/users").body() - } - - // ... other endpoints -} -``` - -#### Task 1.2: Update ViewModels - -- [ ] Create `AuthViewModel.kt` for login state -- [ ] Create `UsersViewModel.kt` for user management -- [ ] Create `DashboardViewModel.kt` for statistics -- [ ] Replace mock data with API calls - -#### Task 1.3: Environment Configuration - -- [ ] Create `local.properties` for API URL -- [ ] Add environment variable support -- [ ] Add MOCK_MODE flag for development -- [ ] Configure different URLs for dev/staging/prod - -#### Task 1.4: Update Desktop UI - -- [ ] Replace mock data in `AdminDashboard.kt` -- [ ] Add loading states -- [ ] Add error handling -- [ ] Add retry logic - -**Acceptance Criteria**: - -- ✅ Desktop app connects to backend API -- ✅ Login works with real credentials -- ✅ User CRUD operations use real API -- ✅ Dashboard statistics show real data -- ✅ Error messages display for API failures - ---- - -### Phase 2: Camera Integration (2-3 hours) - -**Priority**: 🟠 HIGH - -#### Task 2.1: Desktop Camera (Java/JavaFX) - -- [ ] Add JavaCV or WebCam Capture library -- [ ] Create camera preview component -- [ ] Capture photo and convert to ByteArray -- [ ] Send to biometric-processor API - -```kotlin -// desktopApp/src/jvmMain/kotlin/CameraCapture.kt -@Composable -fun CameraCapture(onCapture: (ByteArray) -> Unit) { - // Use JavaCV or WebCam Capture - // Show camera preview - // Capture button - // Return image as ByteArray -} -``` - -#### Task 2.2: Mobile Camera (Android) - -- [ ] Use CameraX library -- [ ] Create camera preview composable -- [ ] Capture photo -- [ ] Handle permissions - -```kotlin -// androidApp/src/main/kotlin/CameraCapture.kt -@Composable -fun CameraCapture(onCapture: (ByteArray) -> Unit) { - // Android CameraX implementation - // Request camera permission - // Show preview - // Capture button -} -``` - -#### Task 2.3: Mobile Camera (iOS) - -- [ ] Use AVFoundation via Swift interop -- [ ] Create camera wrapper -- [ ] Expose to Kotlin - -```swift -// iosApp/CameraHelper.swift -class CameraHelper { - func capturePhoto(completion: @escaping (Data) -> Void) { - // AVFoundation implementation - } -} -``` - -**Acceptance Criteria**: - -- ✅ Desktop camera shows live preview -- ✅ Can capture photos on desktop -- ✅ Android camera works (after mobile app created) -- ✅ iOS camera works (after mobile app created) - ---- - -### Phase 3: Mobile App - Android (2-3 weeks) - -**Priority**: 🟡 MEDIUM - -#### Task 3.1: Android Project Setup - -- [ ] Configure `androidApp` module in `build.gradle.kts` -- [ ] Add Android dependencies (Compose, CameraX, etc.) -- [ ] Create Android manifest with permissions -- [ ] Setup Material3 theme - -#### Task 3.2: Android UI - Enrollment Flow - -- [ ] Create `EnrollmentScreen.kt` (Compose) -- [ ] User information form -- [ ] Camera capture integration -- [ ] Photo preview -- [ ] Submit to backend - -#### Task 3.3: Android UI - Verification Flow - -- [ ] Create `VerificationScreen.kt` (Compose) -- [ ] Camera capture -- [ ] Liveness detection UI -- [ ] Results display - -#### Task 3.4: Android UI - User Profile - -- [ ] Create `ProfileScreen.kt` (Compose) -- [ ] View profile -- [ ] Edit profile -- [ ] Biometric settings - -#### Task 3.5: Android Testing - -- [ ] Test on emulator -- [ ] Test on physical device (min Android 8.0) -- [ ] Test camera on various devices -- [ ] Performance testing - -**Acceptance Criteria**: - -- ✅ Android app builds successfully -- ✅ Can enroll users on Android -- ✅ Can verify users on Android -- ✅ Camera works on Android devices -- ✅ UI is responsive and performant - ---- - -### Phase 4: Mobile App - iOS (2-3 weeks) - -**Priority**: 🟢 LOW - -#### Task 4.1: iOS Project Setup - -- [ ] Configure `iosApp` in Xcode -- [ ] Link shared KMP framework -- [ ] Setup SwiftUI views -- [ ] Configure Info.plist with permissions - -#### Task 4.2: iOS UI - Enrollment Flow - -- [ ] Create `EnrollmentView.swift` (SwiftUI) -- [ ] Call shared ViewModels from Swift -- [ ] Camera integration with AVFoundation -- [ ] Submit to backend - -#### Task 4.3: iOS UI - Verification Flow - -- [ ] Create `VerificationView.swift` (SwiftUI) -- [ ] Camera integration -- [ ] Liveness detection UI -- [ ] Results display - -#### Task 4.4: iOS UI - User Profile - -- [ ] Create `ProfileView.swift` (SwiftUI) -- [ ] View/edit profile -- [ ] Biometric settings - -#### Task 4.5: iOS Testing - -- [ ] Test on simulator -- [ ] Test on physical device (min iOS 14) -- [ ] Test camera on various devices -- [ ] Performance testing - -**Acceptance Criteria**: - -- ✅ iOS app builds successfully -- ✅ Can enroll users on iOS -- ✅ Can verify users on iOS -- ✅ Camera works on iOS devices -- ✅ UI follows iOS design guidelines - ---- - -### Phase 5: Security Tab Completion (1-2 hours) - -**Priority**: 🟡 MEDIUM - -#### Task 5.1: Complete Audit Logs Table - -- [ ] Add detailed audit logs table -- [ ] Implement filtering (action type, date range, user) -- [ ] Add pagination -- [ ] Expand JSON details viewer - -#### Task 5.2: Security Analytics - -- [ ] Add failed login trends chart -- [ ] Add geographic access map (if IP geolocation available) -- [ ] Add security score widget - -**Acceptance Criteria**: - -- ✅ Security tab shows comprehensive audit logs -- ✅ Filtering and search work -- ✅ Charts display security trends - ---- - -### Phase 6: Testing & Polish (1 week) - -**Priority**: 🟡 MEDIUM - -#### Task 6.1: Unit Tests (Shared Code) - -- [ ] Test ViewModels -- [ ] Test API client -- [ ] Test data models -- [ ] 80%+ coverage - -#### Task 6.2: UI Tests (Desktop) - -- [ ] Test user flows (enrollment, verification, admin) -- [ ] Test navigation -- [ ] Test error handling - -#### Task 6.3: UI Tests (Mobile) - -- [ ] Test Android UI -- [ ] Test iOS UI -- [ ] Test camera integration - -#### Task 6.4: Performance Optimization - -- [ ] Optimize image loading -- [ ] Reduce app size -- [ ] Improve startup time -- [ ] Memory profiling - -**Acceptance Criteria**: - -- ✅ 80%+ test coverage on shared code -- ✅ All critical flows tested -- ✅ No memory leaks -- ✅ App size optimized - ---- - -## 🧪 Testing Requirements - -### Unit Tests (Shared Module) - -```kotlin -// UsersViewModelTest.kt -class UsersViewModelTest { - @Test - fun fetchUsers_success() - - @Test - fun createUser_success() - - @Test - fun updateUser_success() - - @Test - fun deleteUser_success() -} -``` - -### UI Tests (Desktop) - -```kotlin -// DesktopUITest.kt -@Test -fun testKioskModeEnrollmentFlow() - -@Test -fun testAdminDashboardUserCRUD() - -@Test -fun testAdminDashboardAnalytics() -``` - -### Manual Testing Checklist - -#### Desktop App - -- [ ] Kiosk mode: Enrollment flow works end-to-end -- [ ] Kiosk mode: Verification flow works end-to-end -- [ ] Admin dashboard: All tabs navigate correctly -- [ ] Admin dashboard: User CRUD operations work -- [ ] Admin dashboard: Charts render correctly -- [ ] Settings: All configuration options work - -#### Android App - -- [ ] Enrollment flow works on Android -- [ ] Verification flow works on Android -- [ ] Camera captures photos correctly -- [ ] App works offline (cached data) -- [ ] Permissions requested correctly - -#### iOS App - -- [ ] Enrollment flow works on iOS -- [ ] Verification flow works on iOS -- [ ] Camera captures photos correctly -- [ ] App works offline (cached data) -- [ ] Permissions requested correctly - ---- - -## 🚀 Deployment - -### Desktop App Deployment - -#### Build Commands - -```bash -# Build desktop app (JVM) -./gradlew desktopApp:packageDistributionForCurrentOS - -# Output: -# - Windows: .exe installer -# - macOS: .dmg installer -# - Linux: .deb or .rpm package -``` - -#### Distribution - -```bash -# Create installers -./gradlew desktopApp:package -# Or use jpackage manually - -# Artifacts: -# - desktopApp/build/compose/binaries/main/ -# - app/ (portable) -# - installer/ (.exe, .dmg, .deb) -``` - -### Android App Deployment - -#### Build APK - -```bash -./gradlew androidApp:assembleRelease - -# Output: -# androidApp/build/outputs/apk/release/androidApp-release.apk -``` - -#### Build App Bundle (for Google Play) - -```bash -./gradlew androidApp:bundleRelease - -# Output: -# androidApp/build/outputs/bundle/release/androidApp-release.aab -``` - -### iOS App Deployment - -#### Build Archive (Xcode) - -```bash -# In Xcode: -# Product → Archive -# Distribute App → App Store Connect or Ad Hoc -``` - -#### Build from Command Line - -```bash -xcodebuild archive -workspace iosApp.xcworkspace \ - -scheme iosApp -archivePath build/iosApp.xcarchive - -xcodebuild -exportArchive -archivePath build/iosApp.xcarchive \ - -exportPath build -exportOptionsPlist exportOptions.plist -``` - ---- - -## 🔗 Integration Points - -### Backend API Integration (Ktor Client) - -```kotlin -// shared/src/commonMain/kotlin/network/ApiClient.kt - -class ApiClient(private val baseUrl: String) { - suspend fun login(email: String, password: String): AuthResponse { - return client.post("$baseUrl/auth/login") { - setBody(LoginRequest(email, password)) - }.body() - } - - suspend fun enrollUser( - firstName: String, - lastName: String, - email: String, - photo: ByteArray - ): EnrollmentResponse { - return client.post("$baseUrl/biometric/enroll") { - setBody(MultiPartFormDataContent( - formData { - append("firstName", firstName) - append("lastName", lastName) - append("email", email) - append("photo", photo, Headers.build { - append(HttpHeaders.ContentType, "image/jpeg") - }) - } - )) - }.body() - } - - suspend fun verifyUser(employeeId: String, photo: ByteArray): VerificationResponse { - return client.post("$baseUrl/biometric/verify") { - setBody(MultiPartFormDataContent( - formData { - append("employeeId", employeeId) - append("photo", photo, Headers.build { - append(HttpHeaders.ContentType, "image/jpeg") - }) - } - )) - }.body() - } -} -``` - -### Expected API Contracts - -```kotlin -// Auth -POST /api/v1/auth/login → AuthResponse { accessToken, refreshToken, user } - -// Users -GET /api/v1/users → List -POST /api/v1/users → User -PUT /api/v1/users/{id} → User -DELETE /api/v1/users/{id} → Unit - -// Biometric -POST /api/v1/biometric/enroll → EnrollmentResponse { jobId } -POST /api/v1/biometric/verify → VerificationResponse { match, confidence } -GET /api/v1/biometric/enrollments/{jobId} → JobStatus { status, result } - -// Dashboard -GET /api/v1/statistics → DashboardStats { totalUsers, activeUsers, ... } -``` - ---- - -## 📈 Success Criteria - -### Desktop App - -- ✅ Kiosk mode fully functional -- ✅ Admin dashboard shows real data -- ✅ Camera captures photos -- ✅ All CRUD operations work -- ✅ Settings persist correctly - -### Android App - -- ✅ Enrollment and verification work -- ✅ Camera integration successful -- ✅ App runs on Android 8.0+ -- ✅ No crashes or ANRs -- ✅ 90% code shared with desktop - -### iOS App - -- ✅ Enrollment and verification work -- ✅ Camera integration successful -- ✅ App runs on iOS 14+ -- ✅ No crashes -- ✅ 90% code shared with desktop - -### Performance - -- ✅ Desktop app startup < 2s -- ✅ Android app startup < 1s -- ✅ iOS app startup < 1s -- ✅ Camera preview lag < 100ms -- ✅ API calls < 1s (p95) - ---- - -## ✅ Architecture Review Findings - RESOLVED - -**Date**: 2025-11-17 (Review) → 2025-11-18 (Completed) -**Status**: ✅ **REFACTORING COMPLETE - READY FOR FEATURE DEVELOPMENT** - -### Architecture Grade: A+ (Achieved!) - -**Strengths** ✅: - -- Excellent Clean Architecture with proper layer separation -- SOLID principles followed in shared module -- Professional DI setup with Koin -- Modern tech stack (KMP, Compose, Ktor) -- 90% code sharing capability - -**Issues Resolved** ✅: - -1. ✅ **Monolithic UI Files**: AdminDashboard.kt (2,335 → 150 lines) + KioskMode.kt (1,756 → 100 - lines) -2. ✅ **Duplicate Packages**: Consolidated to `com.fivucsas.shared` (-727 lines) -3. ✅ **Low Test Coverage**: 50+ ViewModel unit tests added -4. ✅ **No Component Library**: 14 shared UI components created (Atomic Design) -5. ✅ **Missing Abstractions**: Platform interfaces created (ICameraService, ILogger, ISecureStorage) - -### Phase 0 Results - -**14-day architectural refactoring completed successfully** - -**Achieved ROI**: 1,229% over 12 months - -- Reduced debugging time: $28,800 saved -- Faster feature development: $115,200 saved -- Fewer production bugs: $4,800 saved -- **Total Benefit**: $137,600 (vs $11,200 investment) - -**Next Steps**: Proceed to Phase 1 (Backend Integration) or Phase 3 (Android App) - -**See**: `ARCHITECTURE_REVIEW.md` for complete analysis - ---- - -## 📅 **REVISED** Implementation Timeline - -### **PHASE 0: Architectural Refactoring (14 days)** ✅ COMPLETE - -**Goal**: Professional-grade codebase before feature development - -| Sub-Phase | Tasks | Time | Status | -|-----------|-------------------------|--------|------------| -| **0.1** | Package Consolidation | 1 day | ✅ Complete | -| **0.2** | Extract Configuration | 1 day | ✅ Complete | -| **0.3** | Shared UI Components | 2 days | ✅ Complete | -| **0.4** | Refactor AdminDashboard | 3 days | ✅ Complete | -| **0.5** | Refactor KioskMode | 2 days | ✅ Complete | -| **0.6** | Platform Abstractions | 2 days | ✅ Complete | -| **0.7** | ViewModel Tests | 2 days | ✅ Complete | -| **0.8** | Documentation | 1 day | ✅ Complete | - -**Completed**: November 18, 2025 - -**Deliverables**: - -- ✅ AdminDashboard: 2,335 → 150 lines (20+ new files) -- ✅ KioskMode: 1,756 → 100 lines (12+ new files) -- ✅ 20+ reusable UI components -- ✅ Single package structure (no duplicates) -- ✅ 70%+ test coverage -- ✅ Platform abstractions (ICameraService, ILogger, etc.) -- ✅ Professional-grade codebase - -**Why Phase 0 First?** - -1. Technical debt grows exponentially if not addressed -2. Mobile development will proliferate organizational issues -3. Backend integration easier with clean codebase -4. Enables parallel development without merge conflicts -5. Better foundation for scaling team - ---- - -### **PHASE 1: Desktop Backend Integration (3-4 hours)** 🔴 CRITICAL - -**Prerequisites**: Phase 0 complete - -| Phase | Tasks | Estimated Time | Priority | -|-------------|-----------------------------|----------------|--------------------| -| **Phase 1** | Desktop Backend Integration | 3-4 hours | 🔴 CRITICAL | -| **Phase 2** | Camera Integration | 2-3 hours | 🟠 HIGH | -| **Phase 3** | Android App | 2-3 weeks | 🟡 MEDIUM | -| **Phase 4** | iOS App | 2-3 weeks | 🟢 LOW | -| **Phase 5** | Security Tab Completion | 1-2 hours | 🟡 MEDIUM | -| **Phase 6** | Testing & Polish | 1 week | 🟡 MEDIUM | -| **Total** | | **8-10 weeks** | (includes Phase 0) | - ---- - -## 📞 Next Steps - -### Immediate Actions - -1. Pull latest code from repository -2. Open project in IntelliJ IDEA -3. Run desktop app: `./gradlew desktopApp:run` -4. Verify UI works in mock mode -5. Start Phase 1: Backend integration - -### Development Environment Setup - -```bash -# Clone repository -git clone https://github.com/Rollingcat-Software/mobile-app.git -cd mobile-app - -# Open in IntelliJ IDEA -# File → Open → Select mobile-app folder - -# Run desktop app -./gradlew desktopApp:run - -# Build desktop installer -./gradlew desktopApp:packageDistributionForCurrentOS - -# (Later) Run Android app -./gradlew androidApp:installDebug - -# (Later) Open iOS app in Xcode -open iosApp/iosApp.xcodeproj -``` - ---- - -**Document Version**: 1.1 -**Created**: 2025-11-17 -**Last Updated**: 2025-11-18 -**Owner**: Mobile/Desktop Team -**Review Date**: Weekly during implementation diff --git a/docs/archive/README.md b/docs/archive/README.md index 169d29f6..9f9fc50f 100644 --- a/docs/archive/README.md +++ b/docs/archive/README.md @@ -1,6 +1,6 @@ # Documentation Archive -Historical docs preserved for git history. Content here is **not current** — see top-level `README.md`, `CHANGELOG.md`, `ROADMAP_CLIENT_APPS.md`, and `docs/` for authoritative docs. +Historical docs preserved for git history. Content here is **not current** — see top-level `README.md`, `CHANGELOG.md`, the open GitHub issues, and `docs/` for authoritative docs. ## 2026-04-16