Files
orion/docs/proposals/android-terminal-implementation.md
Samir Boulahtit b27d4ba6ff
Some checks failed
CI / ruff (push) Successful in 15s
CI / pytest (push) Failing after 2h21m3s
CI / validate (push) Successful in 29s
CI / dependency-scanning (push) Successful in 32s
CI / docs (push) Has been skipped
CI / deploy (push) Has been skipped
docs: add Android terminal implementation plan
Full implementation plan for the RewardFlow Terminal Android app:
4 screens (Setup, PIN, Terminal, Enrollment), 6 phases (~5 days),
ASCII wireframes, data layer design, offline queue strategy,
multi-language support, and API model changes needed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 22:52:00 +02:00

338 lines
17 KiB
Markdown

# 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<Int>?` 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"