docs
/
Appmint Mobile

Architecture

How the app boots, how state and networking are layered, and which services talk to which part of the platform.

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:

ProviderOwns
AuthProviderSign-in state, tokens, saved sessions, forced logout
LeadsProviderLead lists and detail (CRM leads endpoints)
ContactsProviderCustomer/contact lists via the repository
InboxProvider(ApiService())Server-threaded conversations and messages
ChatProviderSocket.IO chat sessions, agent status
CommunicationsProviderSMS/call/recording history
EventsProviderEvent admin lists and actions
LocationProviderThe selected business location and its slug
PosProviderMulti-cart POS state, save/fire/settle
OperationsProviderReservations, service points, workflow tasks
OperationsDashboardProviderSLA health; polls every 30 s on its own so queue screens do not rebuild
CheckinProviderLive check-in queue; refuses to load with an empty location
AppVisibilityProviderWhich 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.

  • Bottom tabs — lib/screens/home/home_screen.dart holds an IndexedStack of DashboardScreen, InboxScreen, CalendarScreen and MoreScreen. The bar is HaHoBottomNavigation (lib/widgets/haho_navigation.dart): Home, Inbox, a centre phone FAB that pushes PhoneScreen as 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.dart is the single source of truth. AppDescriptor(id, label, icon, color) entries are grouped into HomeAppGroups: 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) and system (devices). The tile-to-screen map lives in lib/screens/dashboard/dashboard_screen.dart. AppVisibilityProvider filters tiles and whole groups using the device-local home_hidden_groups / home_hidden_items keys.
  • 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 lowercase orgid header. The org id is also what the softphone reads to bind its Twilio identity user:<orgId>:<email>.
  • A 401 triggers one refresh through onTokenRefresh, which AuthProvider wires to POST /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 in SharedPreferences.

Two service styles sit on top of it:

StyleUsed 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
Locations are passed by slug, not id

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

ServiceNotes
pos_service.dartTabs, 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.dartReservations (read through /repository/find/reservation so operators see everyone's bookings), service points, slots, definitions
checkin_service.dartThe /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.dartSocket.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.dartREST presence and queue stats; ChatPresenceProvider polls every 15 s and also listens to presence-change
softphone_service.dart, communications_service.dartTwilio 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.dartmek_stripe_terminal: connection token, locations, reader discovery, Bluetooth or Tap to Pay, card_present intents
nfc_service.dartOn-device NFC or a BLE serial reader, one cardStream of CardTap{cardUid, employeeId, source}
scanner_service.dartCamera, HID keyboard-wedge (non-consuming, 60 ms gap, 3-char minimum) and BLE serial into one scanStream
client_info_service.dartDevice/OS/app snapshot, lazy network type, browser-like user agent string
app_notification_service.dartContext-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:

ClaimReality
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
Biometricslocal_auth is declared and never called
Two-factor / TOTPThe 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-inNone
Payment linksNone
General push notificationsFirebase is used only to wake the app for Twilio call invites on Android; iOS uses PushKit. More → Notifications is a stub
Forgot passwordShows a success toast without calling an endpoint