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>
17 KiB
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:
- CameraX preview with ML Kit barcode scanning
- QR code contains JSON:
{"api_url": "https://rewardflow.lu", "store_code": "FASHIONHUB", "auth_token": "jwt..."} - Or manual entry (for dev/testing)
- On scan/submit: call
GET /api/v1/store/loyalty/programto verify connection - Download and cache: program config, branding (colors, logo), staff PINs, categories
- Save to DataStore: api_url, auth_token, store_id, store_code
- 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 composabledata/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:
- Show list of staff names (from cached PINs, refreshed from API periodically)
- Seller taps their name → name highlighted, keypad appears
- Enter 4-digit PIN → verified locally against bcrypt hash (cached from API)
- If correct → navigate to Terminal with staffPinId + staffName
- If wrong → shake animation, error message, clear digits
- Language selector (globe icon) → switches app language, saved per-seller in DataStore
- Settings gear → shows pending sync count, device info, "Reset device" option
- 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 languagedata/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:
- Search: text field for card number or email, searches via API
- QR Scan: opens camera overlay, scans loyalty card QR → lookup
- Enroll: inline form (name, email, birthday) → creates customer + card
- Customer found: right panel shows card details + action buttons
- 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)
- Earn points: EUR amount input (digits only) + category → API call
- Add stamp: category → API call
- Redeem stamps: confirm dialog → API call
- Redeem reward: reward picker dropdown → API call
- Offline: stamp/earn queued to Room DB, redeem shows error dialog
- Auto-lock: 2 min idle → navigate back to PIN screen
- 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 queueui/terminal/components/CustomerCard.kt— NEW, customer info + balance displayui/terminal/components/ActionButtons.kt— NEW, stamp/points/redeem buttonsui/terminal/components/CategorySelector.kt— NEW, multi-select pill groupui/terminal/components/EnrollForm.kt— NEW, inline enrollmentui/terminal/components/TransactionList.kt— NEW, recent transactionsui/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:
- Shown as a bottom sheet dialog from Terminal screen
- Name + email required, phone + birthday optional
- On submit:
POST /store/loyalty/cards/enroll - On success: auto-selects the new card in the terminal
- Offline: queued to Room DB, card shown with "Pending sync" badge
Data Layer
DataStore (device config — persists across app restarts)
api_url: Stringauth_token: Stringstore_id: Intstore_code: Stringstore_name: Stringprogram_json: String(cached program config)staff_pins_json: String(cached PINs list with bcrypt hashes)categories_json: String(cached categories)is_device_set_up: Booleanseller_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
OneTimeWorkRequestwith network constraint
Implementation Phases
Phase A: Core infrastructure (~1 day)
DeviceConfigRepository— DataStore read/write for all configStaffPinRepository— cache + refresh PINs from APICategoryRepository— cache + refresh categoriesNetworkMonitor— observe connectivity state (ConnectivityManager)AuthInterceptor— OkHttp interceptor that adds Bearer token from DataStore- Add bcrypt dependency for client-side PIN verification
- String resources for EN/FR/DE/LB (all 4 locales)
Phase B: Setup screen (~0.5 day)
- QR scanner composable (CameraX + ML Kit)
- Manual entry form (for dev)
- Connection verification (call program API)
- Download + cache initial config
- Navigate to PIN on success
Phase C: PIN screen (~0.5 day)
- Staff name list (from cached PINs)
- PIN keypad with verification (bcrypt compare)
- Per-seller language preference
- Language selector (globe icon)
- Pending sync count indicator
- Error handling (wrong PIN, locked PIN)
Phase D: Terminal screen (~2 days)
- Two-pane landscape layout
- Search field + API lookup
- QR scanner overlay (reuse from setup)
- Customer card display (balance, stamps, rewards)
- Action buttons (stamp, earn, redeem stamps, redeem reward)
- Category multi-select pills
- EUR input (digits only, same validation as web)
- Reward dropdown picker
- Recent transactions list
- Offline indicator in header
- Auto-lock timer (2 min idle)
Phase E: Offline queue + sync (~0.5 day)
- Queue stamp/earn/enroll to Room when offline
- Show "Pending sync" badges
- SyncWorker with network constraint
- Toast on sync completion
- Redeem shows "offline" error dialog
Phase F: Polish + kiosk (~0.5 day)
- Lock Task Mode setup
- App icon + splash screen
- Error handling (network errors, API errors, toast messages)
- Loading states on all API calls
- Landscape-only enforcement
- 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>?onStampRequestandPointsEarnRequestCategoryResponsemodel for cached categoriesGET /api/v1/store/loyalty/categoriesendpoint inLoyaltyApi.kt
String Resources (multi-language)
4 files:
res/values/strings.xml(EN — default)res/values-fr/strings.xmlres/values-de/strings.xmlres/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"