Cash drawer
- openCashDrawer was a debugPrint. The drawer never opened.
- It cannot go through the PDF pipeline: a PDF is rendered by the platform
driver, which will not pass raw ESC/POS bytes to the device. So it goes over
a socket instead — nearly every network thermal printer listens on 9100 and
forwards whatever arrives straight to the print head, which makes the whole
protocol five bytes.
- Printer IP and port are configurable in Settings with a Test button that
saves and fires immediately, because a drawer that does not open is
indistinguishable from one that is not wired up.
- Every failure explains itself: unreachable, refused, or simply not
configured — which is the honest state for a USB printer, since there is no
raw path to one from Flutter.
- Now fires only on a cash tender. A card-only sale that pops the drawer is a
shrinkage risk, and it is the first thing a shop notices.
Back-office route
- Host, port, TLS and transport persist to the database; username, password
and API key go to the platform keystore (Keychain / Credential Manager /
Android Keystore). Writing credentials into SQLite would put them in the
same file as the bills, on a machine behind a shop counter.
- Loaded at startup. Previously the dialog wrote settings that were silently
ignored on the next launch, which reads exactly like they never saved — and
credentials retyped every morning end up on a sticky note instead.
- A saved route never overwrites the terminal's store or terminal id. Those
belong to the device, and re-pointing a till at a different broker must not
change who it is, or its bills and presence records stop lining up.
Tests: 168 -> 176. The drawer test stands up a real socket server and asserts
the exact bytes arrive. The config test asserts no credential appears anywhere
in the meta table while the non-secret settings do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Settings > Connectivity > Configure now points a terminal at a back office.
Until this existed a store was wired up by editing syncConfigProvider and
rebuilding, which made every terminal in a fleet its own build.
- Transport picker (offline demo / HTTP / MQTT) with only the relevant fields
shown, validated: an MQTT route with no host is refused rather than silently
saved, because a terminal pointed at nothing looks exactly like one that is
merely offline.
- Terminal name and store id are editable and persist to the database. The
device id and terminal code are shown but not editable, with a copy button —
they are what a support call needs, and re-coding a till must not orphan the
bills already written under the old code.
- TLS defaults on, with a note that bills carry customer names and numbers.
- About card now shows the real terminal, device id and store instead of the
literal TERM-01, and the dead "Check for updates" button is now the entry
point to this dialog.
Lints cleared, analyzer now reports zero issues:
- SoundService wrapped a plain bool in a getter and setter that did nothing.
- Two post-await guards used context.mounted inside a State, which the
analyzer cannot relate to the State's own lifetime. Both are now `mounted`.
Tests: 140 -> 141. The new widget test drives the dialog end to end and asserts
that saving an MQTT route with no host keeps the dialog open with the error
visible. Suite run three times clean.
Known gap, documented in docs/sync-contract.md: broker credentials live in
memory and must be re-entered after a restart. Persisting them means
encrypting at rest.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>