Offline-first Flutter mobile application for iOS and Android providing on-the-go procurement management with biometric authentication, push notifications, barcode scanning, and approval workflows.
The ProKure Mobile Application is a Phase 2 companion to the web platform, built with Flutter 3.x for cross-platform deployment on iOS and Android. It enables procurement stakeholders to manage approvals, monitor KPIs, receive real-time notifications, and perform GRN scanning from anywhere. The app follows an offline-first architecture using Hive local storage with a background sync queue, ensuring uninterrupted workflows even without network connectivity.
| Platform | Minimum Version | Target Version | Notes |
|---|---|---|---|
| iOS | iOS 15.0+ | iOS 17.x | Face ID, Touch ID, APNs via FCM |
| Android | Android 12 (API 31)+ | Android 14 (API 34) | Fingerprint, notification channels |
The mobile app uses Flutter 3.x with the BLoC (Business Logic Component) pattern for state management. The architecture follows a clean layered approach separating UI, business logic, data repositories, and data sources.
+---------------------------------------------------------------+
| PRESENTATION LAYER |
| Flutter Widgets | Screens | Dialogs | Bottom Sheets |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| BLoC LAYER |
| AuthBloc | ApprovalBloc | DashboardBloc | SyncBloc | ... |
| (State Management via flutter_bloc) |
+---------------------------------------------------------------+
|
v
+---------------------------------------------------------------+
| REPOSITORY LAYER |
| AuthRepo | ApprovalRepo | DashboardRepo | ScanRepo | ... |
| (Abstracts data source selection: API vs Local) |
+---------------------------------------------------------------+
| |
v v
+-------------------------------+ +----------------------------+
| REMOTE DATA SOURCE | | LOCAL DATA SOURCE |
| REST API (.NET 10 Backend) | | Hive Boxes (Offline DB) |
| Dio HTTP Client | | flutter_secure_storage |
| JWT Token Interceptor | | Sync Queue |
+-------------------------------+ +----------------------------+
|
v
+-------------------------------+
| BACKEND SERVICES |
| .NET 10 API | PostgreSQL |
| MongoDB | Redis |
| FCM | SignalR |
+-------------------------------+
| Package | Purpose | Version |
|---|---|---|
flutter_bloc | State management (BLoC pattern) | ^8.x |
dio | HTTP client with interceptors | ^5.x |
hive_flutter | Offline-first local storage | ^1.x |
flutter_secure_storage | Encrypted token storage (Keychain/Keystore) | ^9.x |
firebase_messaging | Push notifications (FCM) | ^14.x |
local_auth | Biometric authentication | ^2.x |
mobile_scanner | Barcode/QR scanning | ^3.x |
fl_chart | KPI sparklines and charts | ^0.65.x |
connectivity_plus | Network state monitoring | ^5.x |
go_router | Navigation and deep linking | ^12.x |
freezed | Immutable state classes (code gen) | ^2.x |
json_serializable | JSON serialization (code gen) | ^6.x |
The mobile app uses biometric authentication as the primary method with a 6-digit PIN fallback. JWT tokens are stored in platform-specific secure storage (iOS Keychain / Android Keystore) and managed with automatic refresh token rotation.
User enters email/password via the web-style login form. Upon success, the API returns an access token (15-minute expiry) and a refresh token (7-day expiry). Both are stored in flutter_secure_storage. The user is then prompted to enroll biometrics.
// Initial authentication flow final response = await dio.post('/api/auth/login', data: { 'email': email, 'password': password, 'device_id': deviceId, 'platform': Platform.isIOS ? 'IOS' : 'ANDROID', }); await secureStorage.write(key: 'access_token', value: response.accessToken); await secureStorage.write(key: 'refresh_token', value: response.refreshToken);
After initial login, the app prompts for biometric enrollment. The biometric credential is linked to the device-specific secure enclave. A biometric token is generated server-side and stored locally for subsequent logins.
// Biometric enrollment final isAvailable = await localAuth.canCheckBiometrics; final biometrics = await localAuth.getAvailableBiometrics(); // Supports: BiometricType.fingerprint, BiometricType.face if (isAvailable) { final authenticated = await localAuth.authenticate( localizedReason: 'Enroll biometrics for ProKure', options: const AuthenticationOptions(biometricOnly: true), ); if (authenticated) { await registerBiometricToken(deviceId); } }
On app launch, the user authenticates via biometric prompt. The stored biometric token is sent to the server which issues a fresh JWT pair. If biometric fails 3 times, the app falls back to PIN entry.
App Launch
|
v
Biometric Prompt (Fingerprint / Face ID)
|
+-- Success --> POST /api/mobile/auth/biometric
| { biometric_token, device_id }
| --> Returns new JWT pair
|
+-- Fail (3x) --> PIN Entry Screen
| --> POST /api/auth/login (PIN mode)
|
+-- Cancel --> Stay on lock screen
A Dio interceptor automatically attaches the access token to every API request. When a 401 response is received, the interceptor transparently refreshes the token using the refresh token and retries the original request.
// Dio interceptor for token refresh class AuthInterceptor extends Interceptor { void onRequest(options, handler) { options.headers['Authorization'] = 'Bearer $accessToken'; handler.next(options); } void onError(error, handler) async { if (error.response?.statusCode == 401) { final newTokens = await refreshTokens(); // Retry original request with new token final retryResponse = await dio.fetch(error.requestOptions); handler.resolve(retryResponse); } } }
| Token | Expiry | Storage | Rotation |
|---|---|---|---|
| Access Token (JWT) | 15 minutes | flutter_secure_storage | On each refresh |
| Refresh Token | 7 days | flutter_secure_storage | Rotated on use (one-time) |
| Biometric Token | 30 days | flutter_secure_storage | Re-enrolled on expiry |
Each device is registered with the backend on first login. Maximum 3 devices per user. Sessions expire after 30 minutes of inactivity (configurable). The device registration includes FCM token for push notifications.
// Device registration payload { "device_id": "UUID-generated-on-install", "platform": "IOS" | "ANDROID", "os_version": "17.2", "app_version": "1.0.0", "fcm_token": "firebase-cloud-messaging-token", "device_name": "iPhone 15 Pro", "biometric_capable": true, "biometric_type": "FACE_ID" }
A 6-digit PIN is set during onboarding as a fallback when biometric authentication is unavailable or fails. The PIN is hashed locally (SHA-256 + salt) and verified against the server. After 5 failed PIN attempts, the account locks for 15 minutes.
The app implements an offline-first architecture using Hive for local storage and a sync queue for deferred operations. The connectivity_plus package monitors network state and triggers sync when connectivity is restored.
+--------------------+ +--------------------+
| ONLINE MODE | | OFFLINE MODE |
+--------------------+ +--------------------+
| | | |
| User Action | | User Action |
| | | | | |
| v | | v |
| Repository Layer | | Repository Layer |
| | | | | |
| v | | v |
| API Call (Dio) | | Hive Local DB |
| | | | | |
| v | | v |
| .NET 10 Backend | | Sync Queue (Hive) |
| | | | | |
| v | | +-- Stored as |
| Response --> Cache | | pending ops |
| in Hive | | |
+--------------------+ +--------------------+
|
Connectivity Restored
|
v
+--------------------+
| SYNC PROCESSOR |
+--------------------+
| 1. Read queue FIFO |
| 2. POST /sync/push |
| 3. GET /sync/pull |
| 4. Resolve conflicts|
| 5. Update Hive |
| 6. Clear queue |
+--------------------+
updated_at timestamp comparison and row_version optimistic concurrency// Sync queue entry structure class SyncQueueEntry { final String id; // UUID final String entityType; // 'approval', 'pr', 'grn' final String action; // 'approve', 'reject', 'create', 'update' final Map payload; // JSON data final DateTime createdAt; final int retryCount; // Max 3 retries final String status; // 'pending', 'processing', 'failed', 'completed' } // Processing order: FIFO with retry backoff // Retry 1: 5 seconds // Retry 2: 30 seconds // Retry 3: 120 seconds // After 3 failures: mark as 'failed', notify user
| Tier | Caching Strategy | Data Types | Max Size |
|---|---|---|---|
| Tier 1 | Always cached (proactive sync) | User profile, pending approvals, KPI snapshots | ~5 MB |
| Tier 2 | On-demand cache (LRU eviction) | PR/PO details, vendor info, GRN records | ~30 MB |
| Tier 3 | Online-only (no caching) | Reports, analytics drill-down, NLP queries | N/A |
Push notifications are delivered via Firebase Cloud Messaging (FCM) for both iOS and Android. Notifications support deep linking to specific screens and respect user-configured quiet hours.
.NET 10 Backend (Notification Service)
|
v
FCM HTTP v1 API
|
+-- iOS: APNs (via FCM bridge)
| +-- Notification payload
| +-- Badge count update
|
+-- Android: FCM direct
+-- Notification channel routing
+-- Foreground/background handling
| Type | Trigger | Recipient | Deep Link | Priority |
|---|---|---|---|---|
| KPI_ALERT | KPI breaches configured threshold | Dashboard owner | /dashboard/kpi/{code} | HIGH |
| ANOMALY_ALERT | Critical anomaly detected | Procurement admin | /notifications/anomaly/{id} | HIGH |
| APPROVAL_PENDING | Approval waiting > 4 hours | Assigned approver | /approvals/{id} | HIGH |
| DELIVERY_UPDATE | ASN dispatched or delivery confirmed | Receiving team | /po/{id}/delivery | NORMAL |
| BUDGET_WARNING | Budget utilization > 80% | Project manager | /dashboard/budget/{projectId} | HIGH |
// User quiet hours stored in user_analytics_preferences (MongoDB) { "mobile_settings": { "push_enabled": true, "quiet_hours_start": "22:00", // 10 PM local time "quiet_hours_end": "07:00", // 7 AM local time "quiet_hours_override": ["APPROVAL_PENDING"], // These bypass quiet hours "notification_channels": { "KPI_ALERT": true, "ANOMALY_ALERT": true, "APPROVAL_PENDING": true, "DELIVERY_UPDATE": true, "BUDGET_WARNING": true } } }
| Channel ID | Name | Importance | Sound |
|---|---|---|---|
| prokure_approvals | Approvals | HIGH | Default |
| prokure_alerts | Alerts & Warnings | HIGH | Alert tone |
| prokure_updates | Updates | DEFAULT | Silent |
| prokure_general | General | LOW | None |
The mobile app provides a streamlined approval experience with swipe gestures, bulk actions, and offline queuing. Biometric re-authentication is required for all approval actions.
Push notifications for APPROVAL_PENDING include quick-action buttons. On iOS, 3D Touch or long-press reveals "Approve" and "Reject" actions. On Android, inline action buttons are displayed in the expanded notification.
Push Notification Received
|
+-- Tap --> Opens Approval Detail Screen
|
+-- Swipe Right (iOS) / "Approve" Button
| |
| v
| Biometric Prompt
| |
| +-- Success --> POST /approvals/{id}/approve
| +-- Fail --> Open app for manual action
|
+-- Swipe Left (iOS) / "Reject" Button
|
v
Opens Rejection Comment Dialog
Shows complete details including: document summary (PR/PO/Invoice), line items, requester info, budget impact, approval chain status, and attached documents. Approve/Reject buttons at the bottom with required biometric confirmation.
The Approvals Inbox supports multi-select mode. Users can select multiple pending items and approve/reject them in batch. Each bulk action requires a single biometric confirmation. Maximum 20 items per bulk action.
// Bulk approval request POST /api/mobile/approvals/bulk { "action": "APPROVE", "approval_ids": ["uuid-1", "uuid-2", "uuid-3"], "comment": "Bulk approved via mobile", "biometric_verified": true }
Optional comments can be added before approving. Rejection always requires a comment (minimum 10 characters). Comments support text only (no attachments from mobile).
If the current user has an active delegation, the Approvals Inbox shows both their own approvals and delegated items. Delegated items are tagged with a DELEGATED badge and the original approver's name.
When offline, approvals are queued in the sync queue with full payload. Biometric is still required (verified locally). Queued approvals show a PENDING SYNC badge. Upon connectivity, the queue is processed FIFO within 5 minutes.
The mobile dashboard presents six KPI cards in a responsive grid layout with sparkline mini-charts showing 7-day trends. Data is pulled from the cached KPI snapshots (Tier 1) with pull-to-refresh for live updates.
| KPI | Code | Target | Chart Type | Drill-Down |
|---|---|---|---|---|
| Procurement Cycle Time | CYCLE_TIME | < 14 days | Sparkline (7d) | By project/department |
| Cost Savings % | COST_SAVINGS | > 5% | Sparkline (7d) | By category/vendor |
| On-Time Delivery % | ON_TIME_DELIVERY | > 95% | Sparkline (7d) | By vendor |
| Pending Approvals | PENDING_APPROVALS | 0 | Count badge | Opens Approvals Inbox |
| Budget Utilization | BUDGET_UTIL | < 80% | Progress ring | By project |
| Vendor Rating Avg | VENDOR_RATING | > 4.0 | Star rating | Vendor directory |
App Launch / Pull-to-Refresh
|
v
Check connectivity
|
+-- Online --> GET /api/mobile/dashboard
| |
| v
| Update Hive cache (Tier 1)
| |
| v
| Render KPI cards with fl_chart sparklines
|
+-- Offline --> Read from Hive cache
|
v
Show cached data with "Last updated: X min ago" label
|
v
Render KPI cards (sparklines from cached history)
The mobile app provides focused screens for the most critical procurement workflows. Each screen is optimized for mobile interaction patterns with appropriate offline support.
| Screen | Purpose | Actions | Offline | Data Tier |
|---|---|---|---|---|
| Dashboard | KPI overview with 6 metric cards | View, drill-down, refresh | Yes | Tier 1 |
| Approvals Inbox | Pending approval items across all modules | Approve, reject, bulk action, comment | Yes | Tier 1 |
| PR List / Detail | View purchase requisitions, create new PRs | View, create, submit, search, filter | Partial | Tier 2 |
| PO List / Detail | View purchase order details and status | View, search, filter | Partial | Tier 2 |
| GRN Confirmation | Scan and confirm goods receipt | Scan barcode, confirm qty, capture photo | Yes | Tier 2 |
| Vendor Directory | Search and view vendor details | Search, view profile, view rating, call | Partial | Tier 2 |
| Notifications Center | All notifications with read/unread status | View, mark read, deep link, filter | Yes | Tier 1 |
| Reports | View and download generated reports | View list, download PDF/CSV/XLSX | No | Tier 3 |
| Profile & Settings | User profile, notification prefs, cache mgmt | Edit prefs, quiet hours, clear cache, logout | Yes | Tier 1 |
Bottom Navigation Bar (5 tabs)
|
+-- Dashboard (Home)
| +-- KPI Drill-Down
|
+-- Approvals
| +-- Approval Detail
| +-- Bulk Approve
|
+-- Scan (Center FAB)
| +-- Barcode Scanner
| +-- QR Scanner
| +-- GRN Confirmation
|
+-- Documents
| +-- PR List --> PR Detail --> Create PR
| +-- PO List --> PO Detail
| +-- Reports
|
+-- More
+-- Vendor Directory --> Vendor Detail
+-- Notifications Center
+-- Profile & Settings
The mobile app integrates the device camera for barcode and QR code scanning, enabling quick GRN receiving, PO lookup, material stock checks, and vendor quick-access.
At the receiving dock, users scan the PO barcode printed on the delivery documents. The app pulls the PO details and line items, allowing the user to confirm received quantities, flag discrepancies, and capture photos of damaged goods.
Scan PO Barcode (Code 128 / Code 39)
|
v
POST /api/mobile/scan/barcode { value: "PO-2026-00145" }
|
v
Returns PO details + line items
|
v
GRN Confirmation Screen
+-- Confirm qty per line item
+-- Flag discrepancies (short/excess/damaged)
+-- Capture photo evidence (camera)
+-- Submit GRN confirmation
|
v
POST /api/grn/confirm (or queued if offline)
Scan a material barcode to instantly view current stock levels, recent PO history, and price trends. Useful for warehouse staff checking inventory before creating requisitions.
Vendors can share a ProKure QR code (generated in the vendor portal) that links to their profile. Scanning the QR code opens the vendor detail screen with contact info, rating, and recent PO history.
// QR code payload format { "type": "VENDOR", "vendor_id": "uuid-vendor-123", "tenant_id": "uuid-tenant-456" } // POST /api/mobile/scan/qr // Returns: vendor profile with quick-action options
| Format | Use Case | Library |
|---|---|---|
| Code 128 | PO barcodes, material codes | mobile_scanner |
| Code 39 | Legacy material labels | mobile_scanner |
| QR Code | Vendor profiles, deep links | mobile_scanner |
| EAN-13 | Product identification | mobile_scanner |
The mobile app leverages three MongoDB collections (in the prokure_app database) for push notification analytics, user mobile preferences, and AI prediction request logging.
Tracks push notification lifecycle from queue to delivery to open. TTL: 90 days.
Indexes: (tenant_id, user_id, created_at), (notification_type, status), (created_at) TTL
Per-user mobile settings including quiet hours, notification channel toggles, and dashboard preferences. TTL: None (persistent).
Indexes: (user_id) unique, (tenant_id)
Logs AI prediction requests originating from the mobile app (e.g., price prediction on PR creation, vendor recommendation). TTL: 180 days.
Indexes: (tenant_id, model_type, created_at), (user_id), (created_at) TTL
The mobile app communicates with the .NET 10 backend via RESTful API endpoints. All endpoints require JWT authentication. Mobile-specific endpoints are prefixed with /api/mobile/ and include device context headers.
| Method | Endpoint | Description | Auth | Offline |
|---|---|---|---|---|
| POST | /api/mobile/auth/biometric | Authenticate via biometric token | Biometric token | No |
| POST | /api/mobile/auth/refresh | Refresh JWT using refresh token | Refresh token | No |
| POST | /api/mobile/device/register | Register device with FCM token | JWT | No |
| GET | /api/mobile/dashboard | KPI dashboard data with sparkline history | JWT | Cached |
| GET | /api/mobile/approvals | List pending approvals for current user | JWT | Cached |
| POST | /api/mobile/approvals/{id}/approve | Approve a pending item (biometric required) | JWT + Biometric | Queued |
| POST | /api/mobile/approvals/{id}/reject | Reject a pending item with comment | JWT + Biometric | Queued |
| POST | /api/mobile/sync/pull | Pull latest data from server (delta sync) | JWT | No |
| POST | /api/mobile/sync/push | Push queued offline actions to server | JWT | No |
| GET | /api/mobile/notifications | List notifications with pagination | JWT | Cached |
| POST | /api/mobile/scan/barcode | Resolve barcode to PO/material entity | JWT | Partial |
| POST | /api/mobile/scan/qr | Resolve QR code to vendor/entity | JWT | No |
Authorization: Bearer {access_token} X-Device-Id: {device_uuid} X-App-Version: 1.0.0 X-Platform: IOS | ANDROID X-OS-Version: 17.2 X-Timezone: Asia/Kolkata Content-Type: application/json
Core business rules governing the ProKure mobile application behavior, security, and data synchronization.
quiet_hours_override are delivered immediately regardless of quiet hours.X-App-Version header. If the app version is below the minimum, the API returns HTTP 426 (Upgrade Required) with a message directing the user to update. A "soft update" prompt is shown for recommended (non-mandatory) updates, while a "force update" blocks all functionality for critical updates.