mtrust_imz_kit 1.0.0-dev.2
mtrust_imz_kit: ^1.0.0-dev.2 copied to clipboard
Flutter package for IMZ device connectivity with USB wakeup and BLE communication, building on the shared MTrust reader stack.
IMZ-Kit #
[M-Trust IMZ-Kit]Overview #
The IMZ-Kit integrates the M-Trust IMZ-Reader into your Flutter application. The reader shines light onto a security pigment, measures how the pigment responds, and returns a signed measurement that your backend can verify.
Two levels of integration are available:
ImzModalBuilder— a drop-in modal that handles reader discovery, connection, priming, scanning, retries and error messages. One widget, three callbacks.IMZReader— the command API, if you want to build the flow yourself.
Everything speaks URP over a pluggable ConnectionStrategy, so the same code
runs over Bluetooth, USB, or a virtual reader in tests.
Prerequisites #
- Flutter SDK (Dart
^3.11.0) - An M-Trust IMZ-Reader
- Internet access on first use and when a token runs out. Verification consumes scans from a signed token on the device. When it runs dry, the kit fetches a new one from the M-Trust API. See Tokens.
- Platform permissions for your transport — Bluetooth and/or USB. See
mtrust_urp_ble_strategy.
Getting started #
Installation #
flutter pub add mtrust_imz_kit
Add a transport:
flutter pub add mtrust_urp_ble_strategy
flutter pub add mtrust_urp_usb_strategy
A single import gives you the reader, the widgets, the errors, and all of
mtrust_urp_core plus the SEC protobuf types:
import 'package:mtrust_imz_kit/mtrust_imz_kit.dart';
The barrel re-exports only
LdTheme,LdThemeProviderandLdThemedAppBuilderfromliquid_flutter. If you useLdButton,LdTextorLiquidLocalizations, importpackage:liquid_flutter/liquid_flutter.dartas well.
How to use #
Basic Example #
A complete, compiling integration:
import 'package:flutter/material.dart';
import 'package:liquid_flutter/liquid_flutter.dart';
import 'package:mtrust_imz_kit/mtrust_imz_kit.dart';
import 'package:mtrust_urp_ble_strategy/mtrust_urp_ble_strategy.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return LdThemeProvider(
child: LdThemedAppBuilder(
appBuilder: (context, theme) => MaterialApp(
theme: theme,
localizationsDelegates: const [
ImzLocalizations.delegate,
UrpUiLocalizations.delegate,
LiquidLocalizations.delegate,
],
supportedLocales: ImzLocalizations.supportedLocales,
home: const VerifyPage(),
),
),
);
}
}
class VerifyPage extends StatefulWidget {
const VerifyPage({super.key});
@override
State<VerifyPage> createState() => _VerifyPageState();
}
class _VerifyPageState extends State<VerifyPage> {
final _strategy = UrpBleStrategy();
@override
void dispose() {
_strategy.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: ImzModalBuilder(
strategy: _strategy,
// Bound into the signed result — use your order/item id.
payload: 'ORDER-4711',
onVerificationDone: (measurement) {
final m = measurement.measurement;
debugPrint('reader ${m.readerSn}, payload ${m.payload}');
for (final r in m.result) {
debugPrint('${r.modelId}: score ${r.scoreDistance}');
}
},
onVerificationFailed: (exception) {
debugPrint('failed (${exception.type.name}): ${exception.message}');
},
onDismiss: () => debugPrint('dismissed'),
builder: (context, openModal) => LdButton(
onPressed: () => openModal(),
child: const Text('Verify'),
),
),
),
);
}
}
That is the whole integration. The modal finds a reader, connects, primes, scans, and calls you back.
Localization #
Register the delegate and supported locales, as above. The kit ships English
and German. DeviceConnector needs UrpUiLocalizations.delegate, and error
localization falls back to LiquidLocalizations — register all three.
To override strings, subclass ImzLocalizations and put your own delegate
before ImzLocalizations.delegate; Localizations resolves per type using
the first matching delegate.
Adding UI dependencies #
The kit's widgets are built on liquid_flutter. LdTheme.of(context) must
resolve, so wrap your app in LdThemeProvider (see the example above). Without
it the modal throws on open.
Use the IMZ Modal #
ImzModalBuilder takes twelve parameters. Five are required:
| Parameter | Type | Default | Notes |
|---|---|---|---|
strategy |
ConnectionStrategy |
required | BLE, USB, composite, virtual |
payload |
String |
required | Bound into the signed measurement, max 64 chars |
onVerificationDone |
void Function(UrpSecSecureMeasurement) |
required | Success |
onVerificationFailed |
void Function(ImzReaderException) |
required | Failure |
builder |
Widget Function(BuildContext, Function openSheet) |
required | Renders your trigger |
onDismiss |
void Function()? |
null |
User closed the sheet |
disconnectOnClose |
bool |
true |
Ignored for wired readers — see below |
turnOffOnClose |
bool |
true |
Powers the reader down on close |
canDismiss |
bool |
true |
false also blocks the Android back button |
storageAdapter |
StorageAdapter? |
null |
Defaults to shared preferences |
readerConnectorMode |
ReaderConnectorMode |
preferLastConnected |
ephemeral, pair, preferLastConnected |
tokenAmount |
int? |
null |
Scans to request when refilling (device default 10) |
Wired readers are exempt from teardown. If the strategy reports
isWired (i.e. USB), disconnectOnClose and turnOffOnClose are ignored and
the connection is kept. Reconnecting over USB costs a mandatory settle after
opening the port reboots the reader's USB peripheral, and a cabled reader is
owned by the app, not by the sheet. Wireless readers are powered down and
disconnected as documented.
Custom UI Example for iOS (10 step integration) #
If the modal does not fit, drive IMZReader yourself. The whole flow is two
commands:
final reader = IMZReader(connectionStrategy: strategy);
// 1. Prime: binds your payload and reserves a scan from the token.
final primed = await reader.prime('ORDER-4711');
debugPrint('${primed.gsa} scans left on this token');
// 2. Measure: returns when the pigment has been read.
final secure = await reader.startMeasurement();
debugPrint('reader ${secure.measurement.readerSn}');
Connecting, if you are not using DeviceConnector:
// Throws ImzReaderException(connectionFailed) instead of returning false.
final reader = await IMZReader.findAndConnect(strategy);
// Or enumerate first:
await for (final device in IMZReader.findReaders(strategy)) {
debugPrint('${device.name} (${device.transportName})');
}
There is no reader.disconnect() — disconnecting belongs to the strategy:
await strategy.disconnectDevice();
API Reference #
IMZReader #
IMZReader({
required ConnectionStrategy connectionStrategy,
UrpDeviceIdentifier? target, // defaults to urpReader/urpImz
UrpDeviceIdentifier? origin, // defaults to urpHost/urpMobile|urpDesktop
})
Constructing one is cheap and stateless — the kit itself creates a fresh
instance per action. Note the capitalisation: IMZReader, not ImzReader.
Verification
| Method | Returns | Notes |
|---|---|---|
prime(String payload) |
UrpSecPrimeResponse |
.gsa = scans left. Auto-refills tokens |
startMeasurement() |
UrpSecSecureMeasurement |
Blocks until the pigment is read |
stopMeasurement() |
void |
|
unprime() |
void |
|
measureRaw() |
UrpSecRawMeasurement |
Raw ADC waveform, needs the urpRawMeasure capability |
getCounts() |
UrpSecCountsResponse |
|
getModelInfo() |
List<UrpSecModelInfo> |
Tokens
| Method | Returns |
|---|---|
setTokenAmount(int) |
void (sync) |
requestToken() |
UrpSecureToken |
setToken(UrpSecureToken) |
void |
getCurrentToken() |
UrpToken — note: not UrpSecureToken |
Core URP commands — ping, info, getPower, getName, setName,
getPublicKey, getDeviceId, identify, sleep, off, reboot,
stayAwake, unpair, startDFU, stopDFU, connectAP, disconnectAP,
startAP, stopAP.
requestCalibration({double targetOc = 280}) exists but is not production
ready — its signature, AES key and IV are placeholder zero bytes and the
response is discarded.
Reading a result #
void onDone(UrpSecSecureMeasurement secure) {
final m = secure.measurement;
m.readerSn; // reader serial
m.payload; // what you passed to prime()
m.temperature; // °C
m.tokenSn; // token serial
m.tokenScanNr; // which scan of that token — with tokenSn, replay-unique
m.sensorVersion;
for (final r in m.result) {
r.modelId;
r.scoreDistance; // compare against your model thresholds
r.orthogonalDistance;
r.modelVersion;
}
secure.signature; // List<int> — verify server-side
}
The kit does not decide pass/fail. It returns distances; your model thresholds decide, and the signature is what makes the result trustworthy.
Tokens and internet access #
The reader stores a signed token granting a number of scans. prime() spends
one and returns the remainder as gsa.
When the token is exhausted the device answers urpLeaseError, and prime()
recovers automatically: it reads the public key, asks the device for a token
request, exchanges it at the M-Trust API, installs the result, then retries.
This means prime() can require network access, and a failure surfaces as
ImzReaderExceptionType.tokenFailed. The endpoint defaults to
https://api.mtrust.io and is compile-time only:
flutter run --dart-define=API_URL=https://api.dev.mtrust.io
Use tokenAmount / setTokenAmount() to request more scans per refill (device
default is 10) if your users work offline for long stretches.
Errors #
Every failure surfaces as ImzReaderException:
class ImzReaderException implements Exception {
final String? message; // nullable
final ImzReaderExceptionType type;
final Object? cause; // often a DeviceError
}
| Type | Meaning | Retryable |
|---|---|---|
incompatibleFirmware |
Reader firmware too old to parse the result | no |
measurementFailed |
Measurement returned no payload | yes |
tokenFailed |
Token request/refill failed — usually no internet | yes |
connectionFailed |
findAndConnect/connectTo could not connect |
yes |
commandFailed |
A command returned no payload | yes |
unspecified |
Anything not raised by the kit itself | yes |
Unwrap the underlying device error for diagnostics:
void onFailed(ImzReaderException e) {
final cause = e.cause;
if (cause is DeviceError) {
debugPrint('device ${cause.errorCode.name}: ${cause.errorMessage}');
}
}
ImzReaderException.from(dynamic) returns an existing ImzReaderException
unchanged, and otherwise wraps the value as unspecified with cause set.
Connection strategies #
UrpBleStrategy(); // wireless
UrpUsbStrategy(); // wired; isWired == true
CompositeConnectionStrategy([usb, ble]); // try both, first to answer wins
The composite owns its children — dispose the composite, not the children.
It reports isWired and activeTransportName for whichever transport is
actually carrying the connection, which is what exempts USB from teardown.
For tests, UrpVirtualStrategy answers commands from a callback with no
hardware at all — see examples/imz_kit_example/lib/virtual_strategy.dart for
scenarios covering success, nothing detected, rejected commands, incompatible
firmware, failed priming, token failure and an unresponsive reader.
Configuration Options #
readerConnectorMode:ephemeralalways asks the user,pairremembers one reader,preferLastConnected(default) reconnects to the last one used.storageAdapter: where that choice is persisted. Defaults toSharedPrefsStorageAdapter. Note onlyaddress,nameandtypesurvive persistence —transportNameandserialNumberdo not.tokenAmount: scans requested per refill.canDismiss: setfalseto force the flow to completion.API_URL: compile-time backend override via--dart-define.
Troubleshooting #
- Modal throws on open —
LdThemeProvideris missing above it. LdButton/LiquidLocalizationswon't resolve — importliquid_flutterdirectly; the barrel only re-exports three theme symbols.tokenFailed— no route to the token API, or the wrongAPI_URL.incompatibleFirmware— the reader's firmware predates the current measurement format. Update it; retrying will not help.- Reader stays connected after closing the sheet — expected over USB; see the note under Use the IMZ Modal.
InvalidProtocolBufferExceptionfromgetCurrentToken()— it returnsUrpToken, notUrpSecureToken.- No readers found over USB on Android — the app needs USB permission; enterprise policies may suppress the permission dialog entirely.
Notes for AI agents #
If you are an AI assistant integrating this package, the following are the mistakes most often made — several of them appear in older documentation.
Get these right
- The class is
IMZReader(capital I-M-Z).ImzReaderdoes not exist and will not compile. startMeasurement()returnsUrpSecSecureMeasurement, notUrpSecMeasurement. The measurement fields are one level down, at.measurement; the signature is at.signature.ImzModalBuilder.onVerificationDoneisvoid Function(...), whileImzWidget.onVerificationDoneisFuture<void> Function(...). They are not interchangeable.getCurrentToken()returnsUrpToken;requestToken()returnsUrpSecureToken. Parsing one as the other throws at runtime.- The virtual transport package is
mtrust_urp_virtual_strategy, notmtrust_virtual_strategy. IMZReaderhas nodisconnect(). Usestrategy.disconnectDevice().
Do not assume
- Do not assume the barrel import provides all of
liquid_flutter. It exports exactlyLdTheme,LdThemeProvider,LdThemedAppBuilder. - Do not assume
disconnectOnClose: truedisconnects. It is ignored whenstrategy.isWired. - Do not assume
prime()is offline-safe. It may call the M-Trust API to refill a token, and it retries itself recursively with no depth limit. - Do not present
scoreDistanceas a pass/fail verdict. The kit returns distances only; thresholds are the caller's. - Do not use
requestCalibration()in production code — its trust material is placeholder bytes. ImzResultDismissedis declared but never constructed; a dismissal arrives asonDismiss.
Minimum viable integration — LdThemeProvider → ImzModalBuilder with
strategy, payload, onVerificationDone, onVerificationFailed, builder.
Register ImzLocalizations.delegate and UrpUiLocalizations.delegate. Do not
hand-roll discovery or connection; the modal does both.
Verify before asserting. The authoritative source is
packages/mtrust_imz_kit/lib/ — imz_reader.dart for commands,
ui/imz_modal.dart for the modal contract, imz_reader_exception.dart for
errors. Prefer reading those over reproducing snippets from documentation.
Contributing #
We welcome contributions! Please fork the repository and submit a pull request with your changes. Ensure that your code adheres to our coding standards and includes appropriate tests.
License #
This project is licensed under the Apache 2.0 License. See the LICENSE file for details.