halo_sdk_ui 0.2.5
halo_sdk_ui: ^0.2.5 copied to clipboard
Halo Dot tap-on-phone payments for Flutter. Wraps the native sdk_ui library behind one Dart class, so an app can take a card payment on the phone itself. Android only.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:halo_sdk_ui/halo_sdk_ui.dart';
/// Every call the plugin offers, on one screen.
///
/// The reads that need no SDK — its version, the NFC state, this device's
/// installation id — are shown first, then the bring-up runs behind them, and
/// the buttons drive the rest. Bring-up needs a real Halo JWT, which this app
/// takes from `--dart-define=HALO_JWT=…`; without one `init` fails and the
/// buttons that need a live SDK do nothing, while everything above still reads.
void main() => runApp(const ExampleApp());
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
final _halo = HaloSdkUi();
String _sdkVersion = 'unknown';
bool? _nfc;
String _installationId = 'not registered';
String _lastOutcome = '—';
bool _hasPrinter = false;
String _printerStatus = 'not checked';
/// Whether the NFC tap toggle is on. A tap connects to a printer the device
/// is already paired with; the tag only names which one. The plugin owns the
/// NFC mechanism — this just reflects the toggle.
bool _nfcListening = false;
@override
void initState() {
super.initState();
_start();
}
/// Toggles NFC tap listening on or off. One call does it all: the plugin arms
/// NFC dispatch, reads a tapped tag, resolves and selects the printer, and
/// calls the callback — no host NFC code. `enablePrinterTaps` answers whether
/// the device can take a tap (false when there is no NFC adapter).
Future<void> _toggleNfcListening() async {
final listening = !_nfcListening;
try {
if (listening) {
final canTap = await _halo.enablePrinterTaps((device) async {
if (!mounted) return;
if (device == null) {
setState(() => _printerStatus = 'tap resolved no printer');
return;
}
await _checkPrinter();
});
if (!mounted) return;
if (!canTap) {
setState(() => _printerStatus = 'no NFC on this device');
return;
}
} else {
await _halo.disablePrinterTaps();
}
} on MissingPluginException {
if (mounted) setState(() => _printerStatus = 'Android only');
return;
}
if (!mounted) return;
setState(() {
_nfcListening = listening;
_printerStatus = listening ? 'listening for taps…' : 'not listening';
});
}
Future<void> _start() async {
final sdkVersion = await _read(_halo.sdkInfo) ?? 'unknown';
final nfc = await _read(_halo.nfc);
final installationId =
await _read(_halo.deviceInstallationId) ?? 'not registered';
// Shown before the slow half: `init` suspends until the SDK settles, so
// waiting for it here would leave the screen empty until it did.
if (!mounted) return;
setState(() {
_sdkVersion = sdkVersion;
_nfc = nfc;
_installationId = installationId;
});
_halo.onTokenRequest(() async {
return const String.fromEnvironment('HALO_JWT');
});
await _read(_halo.prepare);
final outcome = await _read(_halo.init);
debugPrint('init → $outcome');
// Check whether this device has a printer, to enable the print buttons.
// Nothing is bound here — a print binds the device lazily on first use;
// this only asks "is there a printer?" (and warms it as a side effect). On
// a non-MobiPOS device it answers "no printer" and the buttons stay
// disabled, while everything above still works.
await _checkPrinter();
}
/// Asks which printer this device has, to enable the print buttons. No
/// binding here — `getPrinter` auto-detects (and warms) the device; the
/// actual bind happens on the first `print`.
Future<void> _checkPrinter() async {
var has = false;
var status = 'not checked';
try {
final resolved = await _halo.getPrinter();
if (resolved != null) {
has = true;
status = 'ready (${resolved.name})';
} else {
status = 'no printer';
}
} on MissingPluginException {
status = 'Android only';
}
if (!mounted) return;
setState(() {
_hasPrinter = has;
_printerStatus = status;
});
}
/// Scans for external Bluetooth printers (Zebra) and selects one, so the
/// print buttons reach it. A lone printer is auto-selected by the SDK; with
/// several, this picks the first for the demo (a real host shows a chooser).
/// Then re-checks so the status line and the print buttons reflect it.
Future<void> _scanForPrinter() async {
setState(() => _printerStatus = 'scanning…');
try {
final printers = await _halo.scanForPrinters();
if (printers.isEmpty) {
if (mounted) setState(() => _printerStatus = 'no Printer found');
return;
}
// One is auto-selected by the SDK; for more, the demo takes the first.
if (printers.length > 1) await _halo.selectPrinter(printers.first);
} on MissingPluginException {
if (mounted) setState(() => _printerStatus = 'Android only');
return;
}
await _checkPrinter();
}
/// Prints a sample receipt — the shape a "Print Receipt" button builds after
/// an approved charge: a centred title, the tender, a divider, a QR of the
/// reference. A print's failure is a thrown PlatformException, not a null, so
/// this reports the reason rather than going quiet.
Future<void> _printReceipt() async {
try {
await _halo.print(
const Printable.receipt(
Receipt([
TextLine('Halo Dot', size: 34, bold: true, align: PrintAlign.center),
FeedLine(),
TextLine('Approved', align: PrintAlign.center),
DividerLine(),
TextLine('Amount R10.00'),
TextLine('Card **** 1234'),
TextLine('Auth A1B2C3'),
DividerLine(),
QrLine('example-receipt', size: 240),
FeedLine(2),
]),
),
);
if (mounted) setState(() => _printerStatus = 'printed receipt');
} on PlatformException catch (error) {
if (mounted) setState(() => _printerStatus = '${error.code}: ${error.message}');
} on MissingPluginException {
if (mounted) setState(() => _printerStatus = 'MobiPOS/Android only');
}
}
Future<void> _printText() async {
try {
await _halo.print(const Printable.text('Hello from halo_sdk_ui'));
if (mounted) setState(() => _printerStatus = 'printed text');
} on PlatformException catch (error) {
if (mounted) setState(() => _printerStatus = '${error.code}: ${error.message}');
} on MissingPluginException {
if (mounted) setState(() => _printerStatus = 'MobiPOS/Android only');
}
}
/// Prints a PnP `receiptResponse` sample as plain text and reports the
/// outcome in the status line. The four sample buttons below all route here,
/// passing the raw string or the escape-decoded one (see [fixReceiptEscapes])
/// so a print of each can be compared side by side on paper.
Future<void> _printSample(String label, String body, {bool clean = false, bool center = false}) async {
try {
await _halo.print(Printable.text(body, clean: clean, center: center));
if (mounted) setState(() => _printerStatus = 'printed $label');
} on PlatformException catch (error) {
if (mounted) setState(() => _printerStatus = '${error.code}: ${error.message}');
} on MissingPluginException {
if (mounted) setState(() => _printerStatus = 'MobiPOS/Android only');
}
}
/// One call, with the platform's refusals logged rather than thrown: on a
/// platform the plugin does not serve, every one of these is a
/// `MissingPluginException`.
Future<T?> _read<T>(Future<T?> Function() call) async {
try {
return await call();
} on PlatformException catch (error) {
debugPrint('${error.code}: ${error.message}');
} on MissingPluginException {
debugPrint('halo_sdk_ui serves Android only');
}
return null;
}
Future<void> _charge({String? amount, String? presentation}) async {
final result = await _read(
() => _halo.transact(
amount,
'ZAR',
reference: 'example-${DateTime.now().millisecondsSinceEpoch}',
presentation: presentation,
),
);
debugPrint('transact → $result');
if (!mounted) return;
setState(() => _lastOutcome = result?['resultType'] ?? 'no result');
}
@override
Widget build(BuildContext context) {
const example1Response = 'Example Store Branch 000 000 0000
\nExample Street Address 0
\n Example Lic.:000/000000/00000 
\nVAT No. 0000000000
\n--------------TAX INVOICE---------------
\nEXAMPLE PRODUCT 000000 00.00#
\n 
\nTOTAL (1 item) 00.00
\nTap to Pay Contactless 00.00
\n 0000........0000
\n REF: 00000000
\n PAYMENT PROVIDER REF: 000000
\n 
\nRate Gross VAT Net
\n0.0% 00.00 0.00 00.00#
\n 
\n----------------------------------------
\n Thank you for shopping at Demo Store 
\nYou were served by
\nOPERATOR000000
\n Keep your slip as proof of purchase 
\n CUSTOMER CARE LINE: 0000 00 00 00 
\n----------------------------------------
\n TXN Store Cash. Till Date Time 
\n000000 0000 000 000 00.00.00 00:00
\n
\n 
\n 
\n 
\n 
\n 
\n 
\n ';
const example2Response = '
\n 
\n 
\nExample Store Branch 000 000 0000
\nExample Street Address 0
\n Example Lic.:000/000000/00000 
\nVAT No. 0000000000
\nSUSPENDED TRANSACTION
\nCASHIER: OPERATOR000000
\n 
\nEXAMPLE PRODUCT 000000 00.00#
\n 
\nTOTAL (1 item) 00.00
\n----------------------------------------
\n TXN Store Cash. Till Date Time 
\n000000 0000 000 000 00.00.00 00:00
\n
\n 
\n 
\n 
\n 
\n 
\n 
\n ';
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(title: const Text('halo_sdk_ui')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Text(
'SDK $_sdkVersion\n'
'NFC ${switch (_nfc) {
null => 'no hardware',
true => 'on',
false => 'off',
}}\n'
'Installation $_installationId\n'
'Last outcome $_lastOutcome\n'
'Printer $_printerStatus',
),
const Divider(height: 32),
FilledButton(
onPressed: () => _charge(amount: '10.00'),
child: const Text('Charge R10.00'),
),
FilledButton(
onPressed: () => _charge(amount: '10.00', presentation: 'SHEET'),
child: const Text('Charge R10.00, as a sheet'),
),
FilledButton(
onPressed: _charge,
child: const Text("Charge on the SDK's own keypad"),
),
const SizedBox(height: 16),
OutlinedButton(
onPressed: () => _read(_halo.openNfcSettings),
child: const Text('Open NFC settings'),
),
OutlinedButton(
onPressed: () => _read(() => _halo.language('af')),
child: const Text('Speak Afrikaans'),
),
OutlinedButton(
onPressed: () =>
_read(() => _halo.browser('https://docs.halodot.io')),
child: const Text('Open the docs in a browser'),
),
OutlinedButton(
onPressed: () async {
// The handshake a scanner sits inside: the kernel cancels a
// transaction when something else takes the camera unannounced.
await _read(() => _halo.camera(true));
await _read(() => _halo.camera(false));
},
child: const Text('Borrow and return the camera'),
),
OutlinedButton(
onPressed: () => _read(_halo.forgetRegistration),
child: const Text('Forget this terminal registration'),
),
const Divider(height: 32),
// Printing — its own stack, MobiPOS only. Disabled until a printer
// is found, so a non-MobiPOS device shows why in the status line
// above rather than failing on a tap.
FilledButton(
onPressed: _hasPrinter ? _printReceipt : null,
child: const Text('Print Receipt'),
),
OutlinedButton(
onPressed: _hasPrinter ? _printText : null,
child: const Text('Print a line of text'),
),
OutlinedButton(
onPressed: _checkPrinter,
child: const Text('Check for a printer'),
),
OutlinedButton(
onPressed: _scanForPrinter,
child: const Text('Scan for a Zebra printer'),
),
// Tap-to-connect, a toggle: press once to open (start listening for
// taps), again to close. A tapped printer must already be paired.
OutlinedButton(
onPressed: _toggleNfcListening,
child: Text(
_nfcListening ? 'Close NFC Taps' : 'Open to NFC Taps',
),
),
const Divider(height: 32),
OutlinedButton(
onPressed: _hasPrinter
? () =>
_printSample('example 1', example1Response, clean: true)
: null,
child: const Text('Text — Example 1'),
),
OutlinedButton(
onPressed: _hasPrinter
? () =>
_printSample('example 2', example2Response, clean: true)
: null,
child: const Text('Text — Example 2'),
),
OutlinedButton(
onPressed: _hasPrinter
? () => _printSample(
'example 1 centred',
example1Response,
clean: true,
center: true,
)
: null,
child: const Text('Text — Example 1 (centred)'),
),
OutlinedButton(
onPressed: _hasPrinter
? () => _printSample(
'example 2 centred',
example2Response,
clean: true,
center: true,
)
: null,
child: const Text('Text — Example 2 (centred)'),
),
],
),
),
);
}
}