diff --git a/docs/proposals/android-terminal-implementation.md b/docs/proposals/android-terminal-implementation.md new file mode 100644 index 00000000..c114137d --- /dev/null +++ b/docs/proposals/android-terminal-implementation.md @@ -0,0 +1,337 @@ +# Android Terminal App — Implementation Plan + +## Overview + +Native Android tablet POS app for loyalty terminal. 4 screens, landscape-only, kiosk mode. Calls the existing `/api/v1/store/loyalty/*` API — no backend changes. + +**Already scaffolded:** Gradle project compiles, stub screens render on Pixel Tablet emulator. Retrofit API client, Room DB, Hilt DI, all dependencies wired. + +--- + +## Screens & Workflows + +### Screen 1: Setup (one-time) + +``` +┌──────────────────────────────────────────────────────┐ +│ │ +│ RewardFlow Terminal │ +│ │ +│ [Camera Preview — QR Scanner] │ +│ │ +│ Scan the setup QR code from your │ +│ store settings page │ +│ │ +│ ──── or enter manually ──── │ +│ │ +│ API URL: [________________________] │ +│ Store Code: [____________________] │ +│ Auth Token: [____________________] │ +│ │ +│ [Connect] │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +**Flow:** +1. CameraX preview with ML Kit barcode scanning +2. QR code contains JSON: `{"api_url": "https://rewardflow.lu", "store_code": "FASHIONHUB", "auth_token": "jwt..."}` +3. Or manual entry (for dev/testing) +4. On scan/submit: call `GET /api/v1/store/loyalty/program` to verify connection +5. Download and cache: program config, branding (colors, logo), staff PINs, categories +6. Save to DataStore: api_url, auth_token, store_id, store_code +7. Navigate to PIN screen + +**Files:** +- `ui/setup/SetupScreen.kt` — UI (exists, needs real implementation) +- `ui/setup/SetupViewModel.kt` — state (exists, needs expansion) +- `ui/scanner/QrScannerView.kt` — NEW, CameraX + ML Kit composable +- `data/repository/DeviceConfigRepository.kt` — NEW, DataStore read/write + +**Web backend change needed:** +- New endpoint or page: "Generate tablet setup QR" button on store settings page (generates QR with JWT + store info) + +--- + +### Screen 2: PIN / Staff Selection + +``` +┌──────────────────────────────────────────────────────┐ +│ RewardFlow Terminal Fashion Hub [🌐 FR] [⚙️] │ +├──────────────────────────────────────────────────────┤ +│ │ +│ Select your name │ +│ │ +│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ +│ │ Jane │ │ Diana │ │ Eric │ │ +│ │ Owner │ │ Stylist │ │ Sales │ │ +│ └─────────┘ └─────────┘ └─────────┘ │ +│ │ +│ Enter your PIN │ +│ │ +│ [ • ] [ • ] [ ] [ ] │ +│ │ +│ ┌─────┐ ┌─────┐ ┌─────┐ │ +│ │ 1 │ │ 2 │ │ 3 │ │ +│ ├─────┤ ├─────┤ ├─────┤ │ +│ │ 4 │ │ 5 │ │ 6 │ │ +│ ├─────┤ ├─────┤ ├─────┤ │ +│ │ 7 │ │ 8 │ │ 9 │ │ +│ ├─────┤ ├─────┤ ├─────┤ │ +│ │ C │ │ 0 │ │ ← │ │ +│ └─────┘ └─────┘ └─────┘ │ +│ │ +│ 3 pending transactions ⟳ │ +│ │ +└──────────────────────────────────────────────────────┘ +``` + +**Flow:** +1. Show list of staff names (from cached PINs, refreshed from API periodically) +2. Seller taps their name → name highlighted, keypad appears +3. Enter 4-digit PIN → verified locally against bcrypt hash (cached from API) +4. If correct → navigate to Terminal with staffPinId + staffName +5. If wrong → shake animation, error message, clear digits +6. Language selector (globe icon) → switches app language, saved per-seller in DataStore +7. Settings gear → shows pending sync count, device info, "Reset device" option +8. If offline → show "Offline" badge, PIN still works (cached) + +**Files:** +- `ui/pin/PinScreen.kt` — UI (exists, needs seller list + real PIN verification) +- `ui/pin/PinViewModel.kt` — NEW, loads cached PINs, verifies, manages language +- `data/repository/StaffPinRepository.kt` — NEW, caches PINs from API in DataStore + +**Important:** PIN hashes are bcrypt — need to verify client-side. Add `implementation("at.favre.lib:bcrypt:0.10.2")` to gradle deps (pure Java bcrypt, no native deps). + +--- + +### Screen 3: Terminal (main POS) + +``` +┌──────────────────────────────────────────────────────┐ +│ Staff: Diana Fashion Hub [Offline ⚡] [🔒] │ +├──────────────────────┬───────────────────────────────┤ +│ │ │ +│ Search Customer │ No customer selected │ +│ ┌────────────────┐ │ │ +│ │ 🔍 Card / Email│ │ Scan a QR code or search │ +│ └────────────────┘ │ by card number / email │ +│ │ │ +│ [📷 Scan QR Code] │ │ +│ │ │ +│ ─── or ─── │ │ +│ │ │ +│ [+ Enroll Customer] │ │ +│ │ │ +├──────────────────────┤ │ +│ Recent Transactions │ │ +│ ┌────────────────┐ │ │ +│ │ 10:32 +50pts │ │ │ +│ │ 10:15 +1 stamp │ │ │ +│ │ 09:45 Enrolled │ │ │ +│ └────────────────┘ │ │ +│ │ │ +└──────────────────────┴───────────────────────────────┘ +``` + +**After customer found:** + +``` +┌──────────────────────────────────────────────────────┐ +│ Staff: Diana Fashion Hub [Online ✓] [🔒] │ +├──────────────────────┬───────────────────────────────┤ +│ │ Jane Doe │ +│ Search Customer │ jane@example.com [✕] │ +│ ┌────────────────┐ │ Card: 1234-5678-9012 │ +│ │ 🔍 Card / Email│ │ │ +│ └────────────────┘ │ ┌───────────────────────┐ │ +│ │ │ 500 Points │ │ +│ [📷 Scan QR Code] │ │ 3 / 10 Stamps │ │ +│ │ └───────────────────────┘ │ +│ ─── or ─── │ │ +│ │ ┌──────────┐ ┌──────────┐ │ +│ [+ Enroll Customer] │ │ + Stamp │ │ Redeem │ │ +│ │ │ │ │ Stamps │ │ +├──────────────────────┤ ├──────────┤ ├──────────┤ │ +│ Recent Transactions │ │ + Points │ │ Redeem │ │ +│ ┌────────────────┐ │ │ EUR [__] │ │ Reward ▼ │ │ +│ │ 10:32 +50pts │ │ └──────────┘ └──────────┘ │ +│ │ 10:15 +1 stamp │ │ │ +│ │ 09:45 Enrolled │ │ │ +│ └────────────────┘ │ │ +│ │ │ +└──────────────────────┴───────────────────────────────┘ +``` + +**Flow:** +1. **Search**: text field for card number or email, searches via API +2. **QR Scan**: opens camera overlay, scans loyalty card QR → lookup +3. **Enroll**: inline form (name, email, birthday) → creates customer + card +4. **Customer found**: right panel shows card details + action buttons +5. **Actions**: each opens a bottom sheet or dialog with: + - Category selector (multi-select pills — same as web terminal) + - Confirm button + - No separate PIN entry (already authenticated on PIN screen) +6. **Earn points**: EUR amount input (digits only) + category → API call +7. **Add stamp**: category → API call +8. **Redeem stamps**: confirm dialog → API call +9. **Redeem reward**: reward picker dropdown → API call +10. **Offline**: stamp/earn queued to Room DB, redeem shows error dialog +11. **Auto-lock**: 2 min idle → navigate back to PIN screen +12. **Lock button** (top right): immediate lock to PIN screen + +**Files:** +- `ui/terminal/TerminalScreen.kt` — UI (exists as stub, needs full implementation) +- `ui/terminal/TerminalViewModel.kt` — NEW, state management, API calls, offline queue +- `ui/terminal/components/CustomerCard.kt` — NEW, customer info + balance display +- `ui/terminal/components/ActionButtons.kt` — NEW, stamp/points/redeem buttons +- `ui/terminal/components/CategorySelector.kt` — NEW, multi-select pill group +- `ui/terminal/components/EnrollForm.kt` — NEW, inline enrollment +- `ui/terminal/components/TransactionList.kt` — NEW, recent transactions +- `ui/scanner/QrScannerOverlay.kt` — NEW, fullscreen camera overlay for QR scan + +--- + +### Screen 4: Enrollment (inline or dialog) + +``` +┌──────────────────────────────────────┐ +│ Enroll New Customer │ +│ │ +│ Name * [____________________] │ +│ Email * [____________________] │ +│ Phone [____________________] │ +│ Birthday [____________________] │ +│ │ +│ [Cancel] [Enroll] │ +└──────────────────────────────────────┘ +``` + +**Flow:** +1. Shown as a bottom sheet dialog from Terminal screen +2. Name + email required, phone + birthday optional +3. On submit: `POST /store/loyalty/cards/enroll` +4. On success: auto-selects the new card in the terminal +5. Offline: queued to Room DB, card shown with "Pending sync" badge + +--- + +## Data Layer + +### DataStore (device config — persists across app restarts) +- `api_url: String` +- `auth_token: String` +- `store_id: Int` +- `store_code: String` +- `store_name: String` +- `program_json: String` (cached program config) +- `staff_pins_json: String` (cached PINs list with bcrypt hashes) +- `categories_json: String` (cached categories) +- `is_device_set_up: Boolean` +- `seller_language_{pin_id}: String` (per-seller language preference) + +### Room DB (offline transaction queue) +Already created: `PendingTransaction` entity + DAO. +- type: "stamp", "points_earn", "enroll" +- requestJson: full API request body +- status: pending → syncing → synced / failed + +### WorkManager (background sync) +- `SyncWorker.kt` — NEW, runs when connectivity returns +- Processes pending transactions in order (FIFO) +- Retries failed ones with backoff +- Registered as `OneTimeWorkRequest` with network constraint + +--- + +## Implementation Phases + +### Phase A: Core infrastructure (~1 day) +1. `DeviceConfigRepository` — DataStore read/write for all config +2. `StaffPinRepository` — cache + refresh PINs from API +3. `CategoryRepository` — cache + refresh categories +4. `NetworkMonitor` — observe connectivity state (ConnectivityManager) +5. `AuthInterceptor` — OkHttp interceptor that adds Bearer token from DataStore +6. Add bcrypt dependency for client-side PIN verification +7. String resources for EN/FR/DE/LB (all 4 locales) + +### Phase B: Setup screen (~0.5 day) +1. QR scanner composable (CameraX + ML Kit) +2. Manual entry form (for dev) +3. Connection verification (call program API) +4. Download + cache initial config +5. Navigate to PIN on success + +### Phase C: PIN screen (~0.5 day) +1. Staff name list (from cached PINs) +2. PIN keypad with verification (bcrypt compare) +3. Per-seller language preference +4. Language selector (globe icon) +5. Pending sync count indicator +6. Error handling (wrong PIN, locked PIN) + +### Phase D: Terminal screen (~2 days) +1. Two-pane landscape layout +2. Search field + API lookup +3. QR scanner overlay (reuse from setup) +4. Customer card display (balance, stamps, rewards) +5. Action buttons (stamp, earn, redeem stamps, redeem reward) +6. Category multi-select pills +7. EUR input (digits only, same validation as web) +8. Reward dropdown picker +9. Recent transactions list +10. Offline indicator in header +11. Auto-lock timer (2 min idle) + +### Phase E: Offline queue + sync (~0.5 day) +1. Queue stamp/earn/enroll to Room when offline +2. Show "Pending sync" badges +3. SyncWorker with network constraint +4. Toast on sync completion +5. Redeem shows "offline" error dialog + +### Phase F: Polish + kiosk (~0.5 day) +1. Lock Task Mode setup +2. App icon + splash screen +3. Error handling (network errors, API errors, toast messages) +4. Loading states on all API calls +5. Landscape-only enforcement +6. Hide navigation/status bars (immersive mode) + +**Total: ~5 days** + +--- + +## Web Backend — One Change Needed + +**Generate setup QR code** — new button on the store settings page that creates a QR containing `{"api_url": "...", "store_code": "...", "auth_token": "..."}`. The auth_token should be a long-lived store API JWT (or a device-specific token). + +This could be a simple endpoint: `POST /api/v1/store/loyalty/device-setup-token` that returns a JWT with a 1-year expiry, then the settings page renders it as a QR code. + +--- + +## API Models to Add + +The existing `ApiModels.kt` is missing: +- `category_ids: List?` on `StampRequest` and `PointsEarnRequest` +- `CategoryResponse` model for cached categories +- `GET /api/v1/store/loyalty/categories` endpoint in `LoyaltyApi.kt` + +--- + +## String Resources (multi-language) + +4 files: +- `res/values/strings.xml` (EN — default) +- `res/values-fr/strings.xml` +- `res/values-de/strings.xml` +- `res/values-lb/strings.xml` + +Key strings (~50): +- Setup: "Scan QR code", "Connect", "Connecting..." +- PIN: "Select your name", "Enter your PIN", "Wrong PIN", "PIN locked" +- Terminal: "Search customer", "Scan QR", "Enroll customer", "No customer selected" +- Actions: "Add stamp", "Earn points", "Redeem stamps", "Redeem reward", "Purchase amount" +- Categories: "Select category" +- Status: "Online", "Offline", "Syncing", "X pending" +- Errors: "Connection failed", "No internet", "Try again"