itc_sunmi_card_reader 1.1.0
itc_sunmi_card_reader: ^1.1.0 copied to clipboard
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.
⚠️ 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 amountonStatusUpdate(Function(String)?, optional): Status update callbackonCardDetected(Function(String)?, optional): Card detection callbackonAppSelectionRequired(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:
- Width: Keep receipt width around 260-280 pixels for optimal printing
- Background: Always use white background (
Colors.white) - Text Color: Use black or dark colors for text
- Font Sizes: Use 12-18pt fonts for readability
- Padding: Add adequate padding (12-16px) for clean margins
- Dividers: Use solid lines (
Containerwith height: 1-2) for sections - 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:
- Website: www.itconsortiumgh.com
- Email: apps@itconsortiumgh.com
- Company: ITC Consortium Ghana
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