Appmint Mobile is a single Flutter codebase (appmint_go/appmint_mobile, package appmint_mobile) built on Provider + ChangeNotifier, one HTTP client that carries the platform's two-token auth, and a thick services layer per module. There is no offline store and no navigation framework — screens are pushed with MaterialPageRoute, and the four bottom tabs live in an IndexedStack.
Boot sequence
lib/main.dart runs, in order:
1. Firebase, Android only. Firebase.initializeApp is guarded by Platform.isAndroid. iOS deliberately skips it: inbound-call wake-ups use PushKit/APNs through twilio_voice, and initializing Firebase without config throws [core/not-initialized]. A top-level _firebaseMessagingBackgroundHandler (@pragma('vm:entry-point')) exists only as a no-op safety net for Twilio's FCM data messages.
2. Client info. clientInfoService.initialize() snapshots device, OS, app version and locale once; the HTTP client attaches it to heartbeats, presence and chat connects.
3. Scanner. scannerService.ensureInitialized() wires the three scan inputs (camera, HID keyboard-wedge, BLE serial).
4. Environment. EnvironmentConfig.setEnvironment(kReleaseMode ? production : development). A debug build always talks to the development endpoint; there is no runtime switch. See Environment.
5. runApp(AppmintApp()).
Providers
AppmintApp is a MultiProvider with thirteen ChangeNotifierProviders, registered in this order:
| Provider | Owns |
|---|---|
AuthProvider | Sign-in state, tokens, saved sessions, forced logout |
LeadsProvider | Lead lists and detail (CRM leads endpoints) |
ContactsProvider | Customer/contact lists via the repository |
InboxProvider(ApiService()) | Server-threaded conversations and messages |
ChatProvider | Socket.IO chat sessions, agent status |
CommunicationsProvider | SMS/call/recording history |
EventsProvider | Event admin lists and actions |
LocationProvider | The selected business location and its slug |
PosProvider | Multi-cart POS state, save/fire/settle |
OperationsProvider | Reservations, service points, workflow tasks |
OperationsDashboardProvider | SLA health; polls every 30 s on its own so queue screens do not rebuild |
CheckinProvider | Live check-in queue; refuses to load with an empty location |
AppVisibilityProvider | Which home tiles and groups are hidden on this device |
AuthWrapper (also in main.dart) is a Consumer<AuthProvider>: initial and loading show a spinner, unauthenticated and error show LoginScreen, and an authenticated user gets HomeScreen after a post-frame call that sets the chat owner email and connects the chat socket.
Navigation
- Bottom tabs —
lib/screens/home/home_screen.dartholds anIndexedStackofDashboardScreen,InboxScreen,CalendarScreenandMoreScreen. The bar isHaHoBottomNavigation(lib/widgets/haho_navigation.dart): Home, Inbox, a centre phone FAB that pushesPhoneScreenas a full-screen dialog, Calendar, More. A floating unread-chat pill sits top-right and jumps back to tab 0. - App launcher —
lib/config/home_apps.dartis the single source of truth.AppDescriptor(id, label, icon, color)entries are grouped intoHomeAppGroups:businessmade(pos, tabs, floor, reservations, checkin, take_payment),crm(pipelines, tasks, contacts, leads, support, live_chat, sms),event(events, scan, info, stats, badge, tickets, schedule, people, manage, book) andsystem(devices). The tile-to-screen map lives inlib/screens/dashboard/dashboard_screen.dart.AppVisibilityProviderfilters tiles and whole groups using the device-localhome_hidden_groups/home_hidden_itemskeys. - No drawer. The More tab is the settings hub. Several of its tiles are stubs today (Notifications, Preferences, Language, Dark Mode).
Networking
lib/services/http_client.dart (AppengineHttpClient) is the only HTTP path.
- Every request carries
Authorization: Bearer <token>and a lowercaseorgidheader. The org id is also what the softphone reads to bind its Twilio identityuser:<orgId>:<email>. - A 401 triggers one refresh through
onTokenRefresh, whichAuthProviderwires toPOST /profile/user/refresh; the original request is retried once. A failed refresh becomes a forced logout: storage is cleared, the navigator pops to the first route, and a six-second warning toast explains why. - Tokens live in
FlutterSecureStorage(access_token,refresh_token); the user record and settings live inSharedPreferences.
Two service styles sit on top of it:
| Style | Used for |
|---|---|
ApiService and per-module services (pos_service.dart, operations_service.dart, checkin_service.dart, events_service.dart, …) | Endpoints with business logic: POS tabs, check-in, reservations, chat, phone |
RepositoryService and DataProvider('<datatype>') | Generic record CRUD over /repository/find/<datatype>, /repository/get, /repository/update-partial: leads, tasks, tickets, customers, service points |
LocationProvider.businessLocationName is the location record's data.name. Every operations, POS and check-in query sends that slug as businessLocationId, because the backend's foreign keys target the slug. Sending the sk returns empty lists with no error.
Services layer
| Service | Notes |
|---|---|
pos_service.dart | Tabs, items, fire, settle, refund, receipts. Unwraps {order, transaction} on settle; parses every money and quantity field through a tolerant number parser because the server has returned strings |
operations_service.dart | Reservations (read through /repository/find/reservation so operators see everyone's bookings), service points, slots, definitions |
checkin_service.dart | The /checkin/* queue: walk-in, from-reservation, assign, leave, no-show, notify, clear |
workflow calls (in operations_service.dart) | Definitions, tasks, advance/complete/move-to, SLA analytics, escalations |
chat_service.dart | Socket.IO namespace <appengineEndpoint>/chat, websocket transport only, auth {token, orgId, chatId?}, 2 s reconnect delay, 10 attempts. Inbound events include authenticate, message, update, status, messages, ai-stream, chat-stream, presence-change, chat-assigned, chat-transferred, chat-ended, agent-assigned, agent-changed, queue-notification, queue-updated, session-expiring, session-expired, token_expired, chat-inactivity, sms.received. Outbound: sendMessage, chat-message, chatRequest, updateMessageStatus, shareStatus, getMessages, set-status, pick-next, transfer-chat, close-chat, takeover-chat, resume-ai, assist-ai, join-chat, leave-chat |
chat_presence_service.dart | REST presence and queue stats; ChatPresenceProvider polls every 15 s and also listens to presence-change |
softphone_service.dart, communications_service.dart | Twilio device registration with a 60 s heartbeat against a 90 s TTL, org-scoped identity, persisted "go offline" flag. iPad skips native registration because CallKit/PushKit are iPhone-only |
printer_service.dart + thermal/ | ESC/POS over flutter_thermal_printer; the thermal/ folder is a conditional-import shim (thermal_printer_io.dart re-exports the plugin, thermal_printer_web.dart is a no-op) so the app compiles for web |
stripe_terminal_service.dart | mek_stripe_terminal: connection token, locations, reader discovery, Bluetooth or Tap to Pay, card_present intents |
nfc_service.dart | On-device NFC or a BLE serial reader, one cardStream of CardTap{cardUid, employeeId, source} |
scanner_service.dart | Camera, HID keyboard-wedge (non-consuming, 60 ms gap, 3-char minimum) and BLE serial into one scanStream |
client_info_service.dart | Device/OS/app snapshot, lazy network type, browser-like user agent string |
app_notification_service.dart | Context-free toasts via a ScaffoldMessengerKey, plus screen registration so a chat toast is suppressed while that chat is open |
Theme
lib/config/haho_theme.dart is the live design system (HaHoColors, HaHoSpacing, HaHoRadius, HaHoTextStyles). AppTheme there aliases HaHoTheme, and darkTheme returns lightTheme — the More tab's Dark Mode tile has nothing behind it. A second, unused AppTheme in lib/config/theme.dart (indigo Material 3) is not imported by main.dart.
What is declared but not implemented
Reading pubspec.yaml or the root README.md gives a more generous picture than the code supports. Do not document or rely on these:
| Claim | Reality |
|---|---|
| Offline caching and queued actions (Hive) | hive, hive_flutter and hive_generator are declared; there is no Hive usage under lib/. Persistence is SharedPreferences and secure storage only |
| Biometrics | local_auth is declared and never called |
| Two-factor / TOTP | The app answers a server challenge (email, SMS or authenticator) with a code sheet, resend and method switch. It does not enrol anyone in TOTP — that is done on the web |
| Social sign-in | None |
| Payment links | None |
| General push notifications | Firebase is used only to wake the app for Twilio call invites on Android; iOS uses PushKit. More → Notifications is a stub |
| Forgot password | Shows a success toast without calling an endpoint |