itc_sunmi_card_reader 1.1.0 copy "itc_sunmi_card_reader: ^1.1.0" to clipboard
itc_sunmi_card_reader: ^1.1.0 copied to clipboard

PlatformAndroid

A Flutter plugin for SUNMI POS card reading functionality. Enables card scanning, EMV processing, and data extraction on SUNMI Android devices only.

ITC SUNMI Card Reader #

A Flutter plugin that provides card reading and receipt printing functionality for SUNMI POS devices. This plugin enables EMV card processing, data extraction, and seamless integration with SUNMI's hardware capabilities including thermal printer support.

pub package License: MIT

⚠️ Important Notice #

This plugin ONLY works on SUNMI Android devices. It will not function on regular Android devices, iOS devices, or other POS terminals. Please ensure you are using a SUNMI device before implementing this plugin.

Features #

Card Scanning - Support for EMV chip cards, magnetic stripe cards, and contactless payments
Data Extraction - Extract card number, expiry date, cardholder name, and service codes
Track Data - Access to Track 1 and Track 2 data
EMV Processing - Full EMV transaction support with app selection
Receipt Printing - Print transaction receipts directly to SUNMI thermal printer
Image Printing - Print custom receipt designs as bitmap images
Real-time Callbacks - Status updates during the card reading process
Multiple Card Types - Supports Visa, MasterCard, UnionPay, Amex, JCB, RuPay
Error Handling - Comprehensive error management and recovery

Supported SUNMI Devices #

  • SUNMI P2 series
  • SUNMI P2 Pro
  • SUNMI P2 Lite
  • SUNMI V2 series
  • Other SUNMI POS terminals with card reading and printing capabilities

Installation #

Add this to your package's pubspec.yaml file:

dependencies:
  itc_sunmi_card_reader: ^1.0.0

Then run:

flutter pub get

Platform Setup #

Android Requirements #

  • Min SDK: 21 (Android 5.0)
  • Target SDK: 34
  • Device: SUNMI POS terminal only

The plugin automatically adds required SUNMI permissions. No additional setup needed.

Usage #

Basic Card Scan #

import 'package:itc_sunmi_card_reader/itc_sunmi_card_reader.dart';

// Simple card scan
final result = await ItcSunmiCardReaderService.startCardScan(amount: 25.50);

if (result != null) {
  print('Card Number: ${result.cardNumber}');
  print('Expiry Date: ${result.expiryDate}');
  print('Cardholder: ${result.cardholderName}');
  print('Card Type: ${result.cardType}');
} else {
  print('Scan cancelled or failed');
}

Card Scan with Status Updates #

import 'package:itc_sunmi_card_reader/itc_sunmi_card_reader.dart';

Future<void> scanCard() async {
  try {
    final result = await ItcSunmiCardReaderService.startCardScan(
      amount: 50.00,
      onStatusUpdate: (status) {
        print('Status: $status');
        // Update your UI with scan progress
      },
      onCardDetected: (cardType) {
        print('Card detected: $cardType');
        // Show detected card type to user
      },
    );

    if (result != null) {
      print('Scan successful!');
      print('Card: ${result.cardNumber}');
      print('Expiry: ${result.expiryDate}');
      print('Name: ${result.cardholderName}');
      print('Track 1: ${result.track1}');
      print('Track 2: ${result.track2}');
    }
  } catch (e) {
    print('Error: $e');
  }
}

Receipt Printing #

The plugin provides a printBitmap method to print transaction receipts or any image to the SUNMI thermal printer.

Basic Receipt Printing

import 'dart:ui' as ui;
import 'package:flutter/rendering.dart';

Future<void> printReceipt(CardScanResult result, double amount) async {
  try {
    // Create a GlobalKey for your receipt widget
    final GlobalKey receiptKey = GlobalKey();
    
    // Build your receipt widget (see complete example below)
    final receiptWidget = RepaintBoundary(
      key: receiptKey,
      child: ReceiptWidget(result: result, amount: amount),
    );
    
    // Wait for widget to render
    await Future.delayed(const Duration(milliseconds: 100));
    
    // Capture the widget as an image
    final boundary = receiptKey.currentContext!.findRenderObject() 
        as RenderRepaintBoundary;
    final image = await boundary.toImage(pixelRatio: 1.5);
    final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
    final imageBytes = byteData!.buffer.asUint8List();
    
    // Print the bitmap
    final success = await ItcSunmiCardReaderService.printBitmap(imageBytes);
    
    if (success) {
      print('Receipt printed successfully!');
    } else {
      print('Print failed');
    }
  } catch (e) {
    print('Print error: $e');
  }
}

Receipt Widget Example

Create a custom receipt widget for your transactions:

import 'package:flutter/material.dart';
import 'package:itc_sunmi_card_reader/itc_sunmi_card_reader.dart';

class ReceiptWidget extends StatelessWidget {
  final CardScanResult result;
  final double amount;

  const ReceiptWidget({
    Key? key,
    required this.result,
    required this.amount,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 260, // Receipt width (adjust for your printer)
      color: Colors.white,
      padding: const EdgeInsets.all(16),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.center,
        children: [
          // Header
          const Text(
            'YOUR BUSINESS NAME',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
              color: Colors.black,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 8),
          const Text(
            'TRANSACTION RECEIPT',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.w600,
              color: Colors.black,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          
          // Divider
          Container(height: 2, color: Colors.black),
          const SizedBox(height: 16),
          
          // Date/Time
          Text(
            'Date: ${DateTime.now().toString().substring(0, 19)}',
            style: const TextStyle(fontSize: 14, color: Colors.black),
          ),
          const SizedBox(height: 16),
          
          // Amount
          const Text(
            'AMOUNT',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Colors.black,
            ),
          ),
          const SizedBox(height: 4),
          Text(
            'GHS ${amount.toStringAsFixed(2)}',
            style: const TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
              color: Colors.black,
            ),
          ),
          const SizedBox(height: 16),
          
          // Divider
          Container(height: 1, color: Colors.grey),
          const SizedBox(height: 16),
          
          // Card Details
          const Text(
            'CARD DETAILS',
            style: TextStyle(
              fontSize: 14,
              fontWeight: FontWeight.bold,
              color: Colors.black,
            ),
          ),
          const SizedBox(height: 12),
          
          _buildRow('Card Type', result.cardType),
          _buildRow('Card Number', _maskCardNumber(result.cardNumber)),
          if (result.expiryDate.isNotEmpty)
            _buildRow(
              'Expiry',
              ItcSunmiCardReaderService.formatExpiryDate(result.expiryDate),
            ),
          
          const SizedBox(height: 16),
          
          // Footer
          Container(height: 2, color: Colors.black),
          const SizedBox(height: 16),
          
          const Text(
            'TRANSACTION APPROVED',
            style: TextStyle(
              fontSize: 16,
              fontWeight: FontWeight.bold,
              color: Colors.black,
            ),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 8),
          const Text(
            'Thank you!',
            style: TextStyle(fontSize: 14, color: Colors.black),
            textAlign: TextAlign.center,
          ),
        ],
      ),
    );
  }

  Widget _buildRow(String label, String value) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 8),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(
            '$label:',
            style: const TextStyle(fontSize: 13, color: Colors.black87),
          ),
          Text(
            value,
            style: const TextStyle(
              fontSize: 13,
              fontWeight: FontWeight.w600,
              color: Colors.black,
            ),
          ),
        ],
      ),
    );
  }

  String _maskCardNumber(String cardNumber) {
    if (cardNumber.length < 8) return cardNumber;
    final first4 = cardNumber.substring(0, 4);
    final last4 = cardNumber.substring(cardNumber.length - 4);
    final maskedMiddle = '*' * (cardNumber.length - 8);
    return '$first4 $maskedMiddle $last4';
  }
}

Handling App Selection #

Future<void> scanCardWithAppSelection() async {
  try {
    final result = await ItcSunmiCardReaderService.startCardScan(
      amount: 100.00,
      onAppSelectionRequired: (apps, selectApp) {
        // Show dialog for user to select payment app
        showDialog(
          context: context,
          builder: (context) => AlertDialog(
            title: Text('Select Payment App'),
            content: Column(
              mainAxisSize: MainAxisSize.min,
              children: apps.asMap().entries.map((entry) {
                return ListTile(
                  title: Text(entry.value),
                  onTap: () {
                    Navigator.pop(context);
                    selectApp(entry.key); // Select the app
                  },
                );
              }).toList(),
            ),
          ),
        );
      },
    );

    if (result != null) {
      // Process payment with selected app
      processPayment(result);
    }
  } catch (e) {
    handleError(e);
  }
}

Complete Example with Printing #

import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:itc_sunmi_card_reader/itc_sunmi_card_reader.dart';

class PaymentScreen extends StatefulWidget {
  @override
  _PaymentScreenState createState() => _PaymentScreenState();
}

class _PaymentScreenState extends State<PaymentScreen> {
  CardScanResult? _scanResult;
  bool _isScanning = false;
  bool _isPrinting = false;
  String _scanStatus = '';
  final GlobalKey _receiptKey = GlobalKey();
  static const double _amount = 25.50;

  Future<void> _startCardScan() async {
    if (!ItcSunmiCardReaderService.isSupported) {
      _showSnackBar('Card reading not supported on this device', Colors.red);
      return;
    }

    setState(() {
      _isScanning = true;
      _scanStatus = 'Initializing...';
      _scanResult = null;
    });

    try {
      final result = await ItcSunmiCardReaderService.startCardScan(
        amount: _amount,
        onStatusUpdate: (status) {
          setState(() => _scanStatus = status);
        },
        onCardDetected: (cardType) {
          setState(() => _scanStatus = '$cardType card detected');
        },
        onAppSelectionRequired: (apps, selectApp) {
          _showAppSelectionDialog(apps, selectApp);
        },
      );

      setState(() {
        _isScanning = false;
        _scanResult = result;
      });

      if (result != null) {
        _showSnackBar('Card scanned successfully!', Colors.green);
      }
    } catch (e) {
      setState(() {
        _isScanning = false;
        _scanStatus = '';
      });
      _showSnackBar('Error: ${e.toString()}', Colors.red);
    }
  }

  Future<void> _printReceipt() async {
    if (_scanResult == null) {
      _showSnackBar('No transaction to print', Colors.orange);
      return;
    }

    setState(() => _isPrinting = true);

    try {
      await Future.delayed(const Duration(milliseconds: 100));

      final boundary = _receiptKey.currentContext!.findRenderObject()
          as RenderRepaintBoundary;
      final image = await boundary.toImage(pixelRatio: 1.5);
      final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
      final imageBytes = byteData!.buffer.asUint8List();

      final success = await ItcSunmiCardReaderService.printBitmap(imageBytes);

      if (success) {
        _showSnackBar('Receipt printed successfully!', Colors.green);
      } else {
        _showSnackBar('Print failed', Colors.red);
      }
    } catch (e) {
      _showSnackBar('Print error: ${e.toString()}', Colors.red);
    } finally {
      setState(() => _isPrinting = false);
    }
  }

  void _showAppSelectionDialog(List<String> apps, Function(int) selectApp) {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => AlertDialog(
        title: Text('Select Payment App'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: apps.asMap().entries.map((entry) {
            return ListTile(
              title: Text(entry.value),
              onTap: () {
                Navigator.pop(context);
                selectApp(entry.key);
              },
            );
          }).toList(),
        ),
      ),
    );
  }

  void _showSnackBar(String message, Color color) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text(message),
        backgroundColor: color,
        duration: Duration(seconds: 3),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('SUNMI Card Reader'),
        backgroundColor: Colors.blue,
      ),
      body: Stack(
        children: [
          Padding(
            padding: EdgeInsets.all(20.0),
            child: Column(
              crossAxisAlignment: CrossAxisAlignment.stretch,
              children: [
                // Amount Display
                Container(
                  padding: EdgeInsets.all(20),
                  decoration: BoxDecoration(
                    color: Colors.blue.withOpacity(0.1),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Column(
                    children: [
                      Text('Amount to Charge'),
                      SizedBox(height: 8),
                      Text(
                        'GHS ${_amount.toStringAsFixed(2)}',
                        style: TextStyle(
                          fontSize: 32,
                          fontWeight: FontWeight.bold,
                          color: Colors.blue,
                        ),
                      ),
                    ],
                  ),
                ),

                SizedBox(height: 30),

                // Scan Button
                ElevatedButton.icon(
                  onPressed: _isScanning ? null : _startCardScan,
                  icon: _isScanning
                      ? SizedBox(
                          width: 20,
                          height: 20,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : Icon(Icons.credit_card),
                  label: Text(_isScanning ? 'Scanning...' : 'Scan Card'),
                ),

                SizedBox(height: 20),

                // Results Display
                if (_scanResult != null) ...[
                  Container(
                    padding: EdgeInsets.all(16),
                    decoration: BoxDecoration(
                      color: Colors.green.withOpacity(0.1),
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          'Card Scan Results',
                          style: TextStyle(
                            fontSize: 18,
                            fontWeight: FontWeight.bold,
                          ),
                        ),
                        SizedBox(height: 16),
                        Text('Card: ${_scanResult!.cardType}'),
                        Text('Number: ${_scanResult!.cardNumber}'),
                        if (_scanResult!.expiryDate.isNotEmpty)
                          Text('Expiry: ${_scanResult!.expiryDate}'),
                      ],
                    ),
                  ),

                  SizedBox(height: 20),

                  // Print Button
                  ElevatedButton.icon(
                    onPressed: _isPrinting ? null : _printReceipt,
                    icon: _isPrinting
                        ? SizedBox(
                            width: 20,
                            height: 20,
                            child: CircularProgressIndicator(strokeWidth: 2),
                          )
                        : Icon(Icons.print),
                    label: Text(_isPrinting ? 'Printing...' : 'Print Receipt'),
                    style: ElevatedButton.styleFrom(
                      backgroundColor: Colors.green,
                    ),
                  ),
                ],
              ],
            ),
          ),

          // Hidden receipt widget for printing
          if (_scanResult != null)
            Positioned(
              left: -10000,
              child: RepaintBoundary(
                key: _receiptKey,
                child: ReceiptWidget(
                  result: _scanResult!,
                  amount: _amount,
                ),
              ),
            ),
        ],
      ),
    );
  }
}

API Reference #

ItcSunmiCardReaderService #

Card Scanning Methods

startCardScan({required double amount, ...})

Starts the card scanning process.

Parameters:

  • amount (double, required): Transaction amount
  • onStatusUpdate (Function(String)?, optional): Status update callback
  • onCardDetected (Function(String)?, optional): Card detection callback
  • onAppSelectionRequired (Function(List

Returns: Future<CardScanResult?>

Example:

final result = await ItcSunmiCardReaderService.startCardScan(
  amount: 50.00,
  onStatusUpdate: (status) => print('Status: $status'),
  onCardDetected: (type) => print('Detected: $type'),
);
stopCardScan()

Stops the current card scanning process.

Returns: Future<void>

Example:

await ItcSunmiCardReaderService.stopCardScan();
selectApp(int index)

Selects a payment app when multiple options are available.

Parameters:

  • index (int): Index of the selected app

Returns: Future<void>

Printing Methods

printBitmap(Uint8List imageBytes)

Prints a bitmap image to the SUNMI thermal printer.

Parameters:

  • imageBytes (Uint8List, required): PNG image data as bytes

Returns: Future<bool> - true if print successful, false otherwise

Example:

final success = await ItcSunmiCardReaderService.printBitmap(imageBytes);
if (success) {
  print('Printed successfully');
}

CardScanResult #

Contains the extracted card data.

class CardScanResult {
  final String cardNumber;        // Card PAN (Primary Account Number)
  final String expiryDate;        // Expiry date in MMYY format
  final String serviceCode;       // 3-digit service code
  final String cardType;          // Card type (Visa, MasterCard, etc.)
  final String cardholderName;    // Cardholder name (if available)
  final String track1;            // Complete Track 1 data
  final String track2;            // Complete Track 2 data
}

Utility Methods #

// Format card number with spaces (1234 5678 9012 3456)
String formatted = ItcSunmiCardReaderService.formatCardNumber(cardNumber);

// Format expiry date (MM/YY)
String expiry = ItcSunmiCardReaderService.formatExpiryDate(expiryDate);

// Get display name for card type
String displayName = ItcSunmiCardReaderService.getCardTypeDisplayName(cardType);

Receipt Design Guidelines #

When designing receipt widgets for printing:

  1. Width: Keep receipt width around 260-280 pixels for optimal printing
  2. Background: Always use white background (Colors.white)
  3. Text Color: Use black or dark colors for text
  4. Font Sizes: Use 12-18pt fonts for readability
  5. Padding: Add adequate padding (12-16px) for clean margins
  6. Dividers: Use solid lines (Container with height: 1-2) for sections
  7. Pixel Ratio: Use 1.5 pixelRatio when capturing the image for good quality

Error Handling #

try {
  // Card scanning
  final result = await ItcSunmiCardReaderService.startCardScan(amount: 50.0);
  if (result != null) {
    // Print receipt
    final printed = await ItcSunmiCardReaderService.printBitmap(imageBytes);
    if (!printed) {
      print('Print failed');
    }
  }
} catch (e) {
  if (e.toString().contains('CARD_SCAN_ERROR')) {
    print('Card scan error: $e');
  } else if (e.toString().contains('PRINT_ERROR')) {
    print('Printer error: $e');
  } else if (e.toString().contains('INVALID_ARGUMENT')) {
    print('Invalid argument: $e');
  } else {
    print('Unknown error: $e');
  }
}

Troubleshooting #

Card Reading Issues #

1. "Card reading not supported"

  • ✅ Ensure you're running on a SUNMI device
  • ✅ Check device has card reading hardware

2. "Payment SDK not connected"

  • ✅ Wait 2-3 seconds after app start for SDK initialization
  • ✅ Try restarting the application

3. "Card scan timeout"

  • ✅ Ensure card is properly inserted/swiped/tapped
  • ✅ Check card reader hardware functionality

Printing Issues #

1. "Print failed" or no output

  • ✅ Ensure printer has paper loaded
  • ✅ Check printer is not in error state
  • ✅ Verify imageBytes are valid PNG data

2. "Receipt prints partially or garbled"

  • ✅ Reduce receipt widget width (try 260px)
  • ✅ Use solid background colors
  • ✅ Avoid complex gradients or shadows

3. "Image capture fails"

  • ✅ Ensure GlobalKey is attached to RepaintBoundary
  • ✅ Wait for widget to render before capturing
  • ✅ Check widget is not off-screen or hidden

Performance Tips #

  • Printer warm-up: First print after boot may be slower
  • Image size: Keep receipts under 300px wide for faster printing
  • Multiple prints: Add small delay between consecutive prints
  • Memory: Release image data after printing to free memory

Requirements #

  • Flutter: >= 3.3.0
  • Dart: >= 3.8.0
  • Android: API level 21+ (Android 5.0+)
  • Device: SUNMI POS terminal with card reading and printing capability

Security & Privacy #

  • 🔒 Local Processing: All card data processed on device
  • 🚫 No Network: No data transmitted to external servers
  • 💾 No Storage: Card data not stored on device
  • 🛡️ Secure: EMV-compliant processing
  • 🖨️ Local Printing: Receipts printed directly to device thermal printer

Support & Contact #

For technical support and inquiries:

License #

This project is licensed under the MIT License - see the LICENSE file for details.


Developed by ITC Consortium Ghana
Empowering businesses with innovative payment solutions

2
likes
140
points
22
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for SUNMI POS card reading functionality. Enables card scanning, EMV processing, and data extraction on SUNMI Android devices only.

Repository

Topics

#sunmi #card-reader #pos #emv #android

Documentation

Documentation
API reference

Funding

Consider supporting this project:

www.itconsortiumgh.com

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on itc_sunmi_card_reader

Packages that implement itc_sunmi_card_reader