halo_sdk_ui 0.2.5 copy "halo_sdk_ui: ^0.2.5" to clipboard
halo_sdk_ui: ^0.2.5 copied to clipboard

PlatformAndroid

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&#xd;\nExample Street Address 0&#xd;\n      Example Lic.:000/000000/00000       &#xd;\nVAT No. 0000000000&#xd;\n--------------TAX INVOICE---------------&#xd;\nEXAMPLE PRODUCT 000000             00.00#&#xd;\n &#xd;\nTOTAL (1 item)                     00.00&#xd;\nTap to Pay Contactless             00.00&#xd;\n  0000........0000&#xd;\n  REF: 00000000&#xd;\n  PAYMENT PROVIDER REF: 000000&#xd;\n &#xd;\nRate      Gross       VAT          Net&#xd;\n0.0%      00.00       0.00         00.00#&#xd;\n &#xd;\n----------------------------------------&#xd;\n   Thank you for shopping at Demo Store   &#xd;\nYou were served by&#xd;\nOPERATOR000000&#xd;\n   Keep your slip as proof of purchase    &#xd;\n   CUSTOMER CARE LINE: 0000 00 00 00    &#xd;\n----------------------------------------&#xd;\n TXN   Store Cash.   Till    Date    Time &#xd;\n000000 0000   000    000   00.00.00  00:00&#xd;\n&#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\n ';
    const example2Response = '&#xd;\n &#xd;\n &#xd;\nExample Store Branch 000 000 0000&#xd;\nExample Street Address 0&#xd;\n      Example Lic.:000/000000/00000       &#xd;\nVAT No. 0000000000&#xd;\nSUSPENDED TRANSACTION&#xd;\nCASHIER: OPERATOR000000&#xd;\n &#xd;\nEXAMPLE PRODUCT 000000             00.00#&#xd;\n &#xd;\nTOTAL (1 item)                     00.00&#xd;\n----------------------------------------&#xd;\n TXN   Store Cash.   Till    Date    Time &#xd;\n000000 0000   000    000   00.00.00  00:00&#xd;\n&#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\n &#xd;\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)'),
            ),
          ],
        ),
      ),
    );
  }
}
1
likes
150
points
638
downloads

Documentation

Documentation
API reference

Publisher

verified publisherhalodot.io

Weekly Downloads

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.

Repository (GitHub)
View/report issues

Topics

#payments #nfc #android #point-of-sale #printing

License

unknown (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on halo_sdk_ui

Packages that implement halo_sdk_ui