mtrust_imz_kit 1.0.0-dev.2 copy "mtrust_imz_kit: ^1.0.0-dev.2" to clipboard
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]

Documentation Status

pub package pub points style: flutter lints

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, LdThemeProvider and LdThemedAppBuilder from liquid_flutter. If you use LdButton, LdText or LiquidLocalizations, import package:liquid_flutter/liquid_flutter.dart as 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: ephemeral always asks the user, pair remembers one reader, preferLastConnected (default) reconnects to the last one used.
  • storageAdapter: where that choice is persisted. Defaults to SharedPrefsStorageAdapter. Note only address, name and type survive persistence — transportName and serialNumber do not.
  • tokenAmount: scans requested per refill.
  • canDismiss: set false to force the flow to completion.
  • API_URL: compile-time backend override via --dart-define.

Troubleshooting #

  • Modal throws on open — LdThemeProvider is missing above it.
  • LdButton/LiquidLocalizations won't resolve — import liquid_flutter directly; the barrel only re-exports three theme symbols.
  • tokenFailed — no route to the token API, or the wrong API_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.
  • InvalidProtocolBufferException from getCurrentToken() — it returns UrpToken, not UrpSecureToken.
  • 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). ImzReader does not exist and will not compile.
  • startMeasurement() returns UrpSecSecureMeasurement, not UrpSecMeasurement. The measurement fields are one level down, at .measurement; the signature is at .signature.
  • ImzModalBuilder.onVerificationDone is void Function(...), while ImzWidget.onVerificationDone is Future<void> Function(...). They are not interchangeable.
  • getCurrentToken() returns UrpToken; requestToken() returns UrpSecureToken. Parsing one as the other throws at runtime.
  • The virtual transport package is mtrust_urp_virtual_strategy, not mtrust_virtual_strategy.
  • IMZReader has no disconnect(). Use strategy.disconnectDevice().

Do not assume

  • Do not assume the barrel import provides all of liquid_flutter. It exports exactly LdTheme, LdThemeProvider, LdThemedAppBuilder.
  • Do not assume disconnectOnClose: true disconnects. It is ignored when strategy.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 scoreDistance as 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.
  • ImzResultDismissed is declared but never constructed; a dismissal arrives as onDismiss.

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.

0
likes
150
points
256
downloads

Documentation

Documentation
API reference

Publisher

verified publishermtrust.io

Weekly Downloads

Flutter package for IMZ device connectivity with USB wakeup and BLE communication, building on the shared MTrust reader stack.

Repository (GitHub)
View/report issues

License

Apache-2.0 (license)

Dependencies

fixnum, flutter, flutter_localizations, intl, liquid_flutter, mtrust_sec_kit, mtrust_urp_ble_strategy, mtrust_urp_core, mtrust_urp_types, mtrust_urp_ui

More

Packages that depend on mtrust_imz_kit