docs
/
Appmint Mobile

Known issues

What the September 2026 test pass fixed, what is still open, and the parts of the codebase that promise more than they do.

Source: the live test pass of 2 September 2026 (TEST_BUSINESSMADE_2026-09-02.md at the app root), driven from Chrome against a local appengine with the demo organization and location garden-bar-brooklyn. All fixes below are in the working tree; none were committed or pushed as part of that pass.

Fixed on 2026-09-02

AreaSymptomCause and fix
Tabs"No open tabs" while the server held 200One order had amountPaid: "5368" as a string; an as num cast threw and emptied the list. Every money and quantity field now goes through a tolerant parser (models/pos_order_model.dart)
TabsEvery card showed $0.00Open tabs carry subtotal only; cards now show displayTotal
Take paymentCharge blocked with "Save the tab before settling"; order kept 0 itemsThe ad-hoc line was written with key productItems (landed at the record root) and the boolean response was parsed as an unsaved order. Key is now data.productItems with data.subtotal/data.total, and the order is re-fetched when the response has no sk (services/pos_service.dart)
Book reservationContinue disabled forever on a definition with no servicesThe app now synthesizes the implicit service the server uses (book_reservation_screen.dart)
Book reservation"Invalid service selected" on every bookingServer: validateReservation matched by id while services are keyed by name; now matches either (crm/reservations.service.ts)
Book reservation"Not a new metrics"App posted a wrapped {datatype, data} body; toCreatePayload() sends flat fields
Check-inAssign returned 500; Notify returned 500Server resolved the point by name and then updated by that name; it now writes back with the resolved sk, and the app sends the sk first (checkin/checkin.service.ts, checkin_screen.dart). Same latent bug fixed in clearServicePoint
ReservationsRows had no date or time; seated rows printed a raw idRows show "Fri, Sep 11 · 8:30 PM – 9:00 PM" and the spot's display name; service points load on screen init
ReservationsList was every booking everGrouped as Today (including walk-ins with no start time), Upcoming and Past (folded)
ReservationsSlot bookings shown 5 hours offSlot bookings store a UTC instant, older records a zone-less wall clock; only zoned values are converted
Check-in from a reservationTask created without a location so it never appeared in the scoped queue; row kept offering Check inServer takes businessLocationId (the picker wins), stamps checkedInAt/checkInTaskId on the booking and answers 409 while the task is live; the row shows "In check-in queue"
Take order after seatingWould have opened a stray cartPOS keys tabs by the table's name; the picker now sends the id to assign and the name to POS, and POS also matches on the display label
WebLists did not scroll with a mouse dragMaterialApp.scrollBehavior accepts mouse, trackpad and stylus
WebBuild impossible because of flutter_thermal_printer FFIConditional-import shim under services/thermal/

Fixed on 2026-09-03 (events)

Driven from Chrome against the same local appengine, event oss-ai-meetup-sf-2026. Also uncommitted.

AreaSymptomCause and fix
Scanner, Check OutEvery camera check-out answered "Ticket not found"App sent {code}; server checkOut read only ticketId. Server now resolves code the same way as check-in (dynamic QR → static code → credential) and still accepts ticketId (event-checkin.service.ts, events.controller.ts)
ScannerNo way to send a zone, so occupancy never movedZone chip row (No zone + one per event zone) under the mode chips; the selection is sent as zone with every scan and manual entry (scanner_screen.dart)
Scanner, Manual LookupSent everything as an email filter and showed the raw lookup responseReplaced by manual check-in/out: a code goes straight through the scan path; an email is looked up and the single (or picked) live ticket's code is sent. Result card now shows the server's reason, not only transport errors, and the ticket type name
ServerGET /events/tickets/:id/qr returned 500 for imported ticketsHMAC was signed with an undefined codeSecret; a secret is minted on first use (event-ticket.service.ts getTicketWithFreshQR)
ServerStatic ticket codes matched upper-case onlyName lookup now matches the code as typed, upper- and lower-cased (resolveTicketFromCode)
Live Dashboard, Activity Log, Overview, Home card"Checked In" always 0; zone list always "No zone data available"Screens read checkedIn/totalCheckedIn and a zones array; the endpoints return uniqueAttendees/successful/total/denied and an object keyed by zone id. Provider exposes checkedInCount; dashboard parses the real shape and shows Scans / Denied tiles and per-zone "current / capacity" (events_provider.dart, event_dashboard_screen.dart)
Issue, Comp, Walk-inServer refused every request (ticketTypeId required)Required ticket-type dropdown from GET /events/:id/ticket-types (ticket_type_field.dart); auto-selected for a single type, inline notice when the event has none; correct bodies for issue, comp (items[{ticketTypeId, quantity:1, …}]) and walkIn
Tickets tabStat tiles overflowed by 34 px; rows showed the ticket-type id and "?" for holderless ticketsFixed-height card removed; rows show the type's title from the loaded types and fall back to email / ticket code
Home, event modeTickets / Schedule / People / Manage tiles all opened the shell on OverviewEventShellScreen(initialTab:) — 2 / 3 / 4 / 5
HomeHeader overflowed with a long location nameGreeting and name truncate inside a Flexible

Still open

  • Web-only tap dead zone. After a browser reload some header and back taps stop registering until the next reload, and back on a top-level screen reloads the page. Not reproduced on a device.
  • Rate limiter. Scripted checks from 127.0.0.1 hit 429 after a burst while the app on the LAN IP was unaffected. Expected behaviour, but it will bite anyone running API scripts next to the app.
  • Demo data limits. austin has no service points; the only workflow in the demo org is Stowbo's Custody Pipeline (so Pipelines shows those tasks); there are no bm_location_product records, so day-parts and 86 have no visible effect there.
  • Not exercised. Tasks, Support and the More tiles were not reached in that pass because of the web tap issue. The camera itself was not exercised on web; scan resolution, check-in and check-out were verified through the API and the manual path.
  • Events, still missing screens. Sell is a "coming soon" snackbar; the phone never sends a scan point (checkpoint); no badge printing or mark-printed; no credential assignment, perks, verify-scan, transfer, cancel, refund or go-live; More → Media does nothing; no scan-to-lead; no display or kiosk mode. A ticket keeps status checked_in after a check-out, so the Tickets tab "In" count is people who ever entered, not people inside.

Sign-in is email first (2026-09-17)

Nobody types an organization any more. POST /profile/user/directory/lookup is public, takes {email} and answers {orgs: [{orgId, displayName}]} most-recently-used first; the app uses it as step one:

ResultWhat the screen does
EmptyStays on the email step: No account found for
One orgStraight to the password. The organization is never shown
SeveralAn Organization picker above the password, first entry preselected

The Site Name field and its .appmint.app suffix are gone, along with the remembered-org lock and its Change button. remembered_org is still written on every sign-in, because POS quick sign-in runs on it: that screen no longer asks for an organization and refuses to work until somebody has signed in with an email and password on the device — Sign in with your email and password first. Changing a device's organization is the same act.

Magic-code sign-in resolves the organization through the same lookup, asking in a dialog when there is more than one.

Verification challenges: fixed, and what it took

Both apps answer the challenge now. Appmint Mobile and Stowbo detect requiresTwoFactor, show a code sheet with resend and a method switch, and post to POST /profile/security/challenge/verify with trustDevice: true. A tokenless sign-in response of any other shape is rejected rather than becoming an empty session. event_app and dfw_errand always handled it.

What the server does. POST /profile/user/signin (and the customer equivalent Stowbo uses) returns no tokens when a challenge is due:

{ "requiresTwoFactor": true, "challengeToken": "…", "twoFactorMethod": "email", "message": "Verification code sent to your email" }

Three things trigger it, and only the second is opt-in:

TriggerSettingWho it challenges
The organization requires 2FAsecuritySettings.enableTwoFactorForUsers / …ForCustomersEvery user or customer
The person enabled 2FA themselvesTheir own 2FA statusJust them
New-device verificationsecuritySettings.enableNewDeviceAuthenticationAnyone signing in on an unrecognised device — no 2FA needed

What it used to do, and what to expect from a build older than this: AuthResponse.fromJson defaulted accessToken to '' and AuthProvider.login set the status to authenticated regardless, so the app entered the signed-in state holding an empty token, 401'd on the first request, and forced a sign-out. The person saw Session expired. Please sign in again. forever and was never asked for the code they had been emailed. It reads as a flapping session, not as a missing verification step — which is why it gets reported as "login is broken".

Two paths still skip the challenge server-side, which is a security note rather than a workaround now:

  • Quick sign-in (employee id + passcode, or an NFC card). finishQuickLogin deliberately bypasses the 2FA and new-device challenge so a busy terminal is not interrupted.
  • Magic code. The magic-link strategy performs no 2FA check at all. A factor that an alternate sign-in path ignores is not enforcing much.

Three defects in the fix itself were only visible when it was run, each found in a browser against a live server:

  1. A TextEditingController disposed while the dialog was still animating out. The code sheet owns its controller in a StatefulWidget.
  2. AuthStatus.twoFactorRequired fell through Appmint Mobile's auth gate in main.dart, rendering the dashboard behind the dialog with no token.
  3. The dialog never opened at all (found 2026-09-17, fixed). Signing in passes through AuthStatus.loading, and the gate swaps LoginScreen for a spinner — which destroys its state. The instance that came back was a new one, so the await showDialog in the old one was dead code, and the challenge message rendered as a red error on a reset form. The pending challenge now lives on the provider (twoFactorEmail), and whichever login screen is mounted raises the sheet from a post-frame check.

Server-side faults found while testing this (2026-09-11)

Testing the client fix meant provoking a real challenge, which is how these surfaced. The first one means nobody has ever completed an emailed 2FA sign-in on this platform.

1. Every emailed or texted sign-in code was rejected as wrong. (Fixed.) TwoFactorService is listed in the providers of both UsersModule and SecurityModule, so Nest built one instance per module — each with its own in-memory verificationCodes map. Sign-in stored the code on the Users copy; POST /profile/security/challenge/verify, which lives in SecurityModule, looked in the Security copy, found nothing, and answered That code is not right. A code obtained by calling challenge/send first did work, because that route stores and verifies on the same instance — which is the tell.

The store is now static, so the split cannot matter however the modules are wired. Verified: sign-in → emailed code → verified → working session. Note it remains process-local: with more than one replica, a code sent by one and verified by another will not be found. Redis is the real fix for a clustered deployment.

2. A staff account with no customer record cannot manage 2FA at all. Every /profile/security/* route answers 404 customer {"email":"…"} not found for a staff-only user. It works for [email protected] only because a customer record happens to share that address — confirmed by creating a throwaway user, watching it 404, then creating a customer with the same email and watching the same routes return 200. The consequence: staff cannot enrol a second factor, see their devices, or read their login history, and per-user 2FA for staff is unreachable. The org-wide enableTwoFactorForUsers switch is the only way staff get challenged.

3. Org-enforced 2FA once accepted any code from anyone who had not enrolled. (Fixed — not by this work.) verifyTwoFactorCode used to return true outright when the account's security record had twoFactorEnabled false, so with enableTwoFactorForUsers on, an account that never enrolled could be signed into with any six digits. The method now checks the emailed or texted code in that case. Reproduced against a live server after the change: a brand-new account with nothing enrolled, challenged org-wide, was refused with a made-up code.

4. SMS 2FA refused itself everywhere. (Fixed.) sendCodeSms gated on a config record with data.type === 'twilio' — a shape nothing in the platform writes. Integrations are config records keyed on data.provider (TwilioProvider), so every organization got SMS not configured, including ones sending SMS happily through other features. The check now looks in the right place and, finding nothing locally, warns and proceeds rather than refusing — the notification pipeline resolves the shared org's gateway, which is how most orgs send.

Also noted: DEV_TWO_FACTOR_OVERRIDE=000000 in development.env accepts a fixed code for any account. It is correctly gated on NODE_ENV=development and logs a warning when used, but it is in a tracked env file — worth knowing it exists before anyone copies that file toward production.

Found while writing the user docs (2026-09-10)

Verified by reading the screens, not by report. Each is user-visible and each is documented in the user pages as behaviour, so the docs and the code agree until these are fixed.

ItemWhereEffect
Sign-out wipes the device configurationStorageService.clearAll snapshots only remembered_org and saved_sessions around prefs.clear()Printer, card reader (and Terminal location), scanner, NFC reader, selected location, home layout and the softphone offline flag are all lost on logout. The worst operational bug on this list — a till has to be re-paired to trade.
ProfileScreen is unreachableNo route or push anywhere in lib/Privacy policy, terms, support, about and Delete Account have no entry point. Account deletion is an App Store 5.1.1(v) requirement.
Email composer cannot sendemail_composer_screen.dart calls context.read<ApiService>(); ApiService is not a registered providerSend fails with a provider exception surfaced as Failed to send email: …. The inbox reply path instantiates ApiService() directly and works.
Sign Up tab is inertlogin_screen.dart sets _selectedTab but never branches on it; RegisterScreen is never referencedThe tab clears the fields and shows the login form. No self-registration exists.
Chat console actions are silent no-ops when the socket is downevery emit in chat_service.dart starts if (_socket == null || !_isConnected) return;Status, takeover, resume, assist, transfer and close appear to work and do nothing. Only the header dot hints at it.
pickNext reports nothingchat_provider.dart only debugPrints on empty queue and failurePick Next and Pick Up look broken when the queue is empty. Pick Up also ignores the row and asks the server for the next one.
Transfer accepts any stringchat_conversation_screen.dartA mistyped agent email pops the screen exactly like a success.
Live Presence does not pollstartPolling() is a legacy alias doing a single loadThe board silently goes stale; the agent dot reflects socket count, not status.
SMS Send enables without a recipientsms_composer_screen.dart guards on the body only, and _sendSms returns silently with an empty ToThe button looks live and nothing happens. All SMS failures share one string, Failed to send SMS.
iPad has no calling and says nothingsoftphone_service.dart skips native voice binding on iPadRegistration succeeds, the dot goes green, calls never ring or place.
event_detail_screen.dart is dead codeNever constructedIts "QR scanner coming soon" and 4-tab layout must not be documented.
Scanner and Accreditation from the home grid have no active eventdashboard_screen.dartZone chips are empty and an empty event id is sent. Users must open the event first — documented as a warning.
Destructive actions with no confirmationAccess cards Revoke / Mark lost, participant Remove, More → Logout, Publish EventOne tap, no undo prompt, and Publish Event gives no result message either.
Type DELETE to confirm fails silentlyprofile_screen.dartA mismatch disables nothing and shows nothing; the button reads as broken.

Declared but not implemented

These come from reading the code against the dependencies and the root README, and they matter for anyone estimating work or writing user-facing copy.

ItemState
Offline caching and queued actionshive is declared; nothing under lib/ uses it. Persistence is SharedPreferences and secure storage
Biometricslocal_auth is declared and unused
Two-factor authenticationHandled: the app detects requiresTwoFactor, shows a code sheet with resend and method switch, and posts to /profile/security/challenge/verify. Enrolment is still web-only
Social sign-inNone
Payment linksNone; card present, cash and manual tenders only
General push notificationsOnly the softphone's VoIP wake-up (FCM on Android, PushKit on iOS). More → Notifications is a stub
Forgot passwordShows success without calling an endpoint
Dark modedarkTheme returns the light theme
More → Preferences, LanguageStubs
Contact edit and add-note"Coming soon" stubs on ContactDetailScreen
bm_location_productRead and written by the app, but absent from the SDK and unused by the server

Security notes

  • /storefront routes are public on the server (class-level @PublicRoute()); see Endpoints.
  • service_point.status is an SDK enum but is written as a free string by the storefront status route, while check-in's assign rejects exactly occupied and reserved. A client that writes a custom status bypasses the seating guard.
  • Dev credentials are hardcoded behind kDebugMode in login_screen.dart.
  • Magic-code sign-in performs no 2FA check, so an organization that requires 2FA is not enforcing it on that path.
  • certs/ holds committed private keys and a Firebase admin JSON.