itc_sunmi_card_reader 1.0.0
itc_sunmi_card_reader: ^1.0.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 functionality for SUNMI POS devices. This plugin enables EMV card processing, data extraction, and seamless integration with SUNMI's hardware capabilities.
⚠️ 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
✅ 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 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 Usage #
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');
}
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');
}
}
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 #
import 'package:flutter/material.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;
String _scanStatus = '';
Future<void> _startCardScan() async {
// Check if device supports card reading
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: 25.50,
onStatusUpdate: (status) {
setState(() {
_scanStatus = status;
});
},
onCardDetected: (cardType) {
setState(() {
_scanStatus = '$cardType card detected - Processing...';
});
},
onAppSelectionRequired: (apps, selectApp) {
_showAppSelectionDialog(apps, selectApp);
},
);
setState(() {
_isScanning = false;
_scanResult = result;
});
if (result != null) {
_showSnackBar('Card scanned successfully!', Colors.green);
} else {
_showSnackBar('Scan cancelled', Colors.orange);
}
} catch (e) {
setState(() {
_isScanning = false;
_scanStatus = '';
});
_showSnackBar('Error: ${e.toString()}', Colors.red);
}
}
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),
),
);
}
Future<void> _stopScan() async {
try {
await ItcSunmiCardReaderService.stopCardScan();
setState(() {
_isScanning = false;
_scanStatus = '';
});
_showSnackBar('Scan cancelled', Colors.orange);
} catch (e) {
print('Error stopping scan: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('SUNMI Card Reader'),
backgroundColor: Colors.blue,
),
body: 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),
border: Border.all(color: Colors.blue.withOpacity(0.3)),
),
child: Column(
children: [
Text(
'Amount to Charge',
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
),
SizedBox(height: 8),
Text(
'GH₵ 25.50',
style: TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
),
SizedBox(height: 30),
// Scan Button
SizedBox(
height: 54,
child: ElevatedButton.icon(
onPressed: _isScanning ? null : _startCardScan,
icon: _isScanning
? SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
)
: Icon(Icons.credit_card, color: Colors.white),
label: Text(
_isScanning ? 'Scanning...' : 'Scan Card',
style: TextStyle(fontSize: 18, color: Colors.white),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
SizedBox(height: 16),
// Cancel Button (only show when scanning)
if (_isScanning)
SizedBox(
height: 48,
child: OutlinedButton(
onPressed: _stopScan,
child: Text('Cancel Scan'),
style: OutlinedButton.styleFrom(
side: BorderSide(color: Colors.red),
foregroundColor: Colors.red,
),
),
),
SizedBox(height: 20),
// Scan Status
if (_isScanning && _scanStatus.isNotEmpty)
Container(
padding: EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
SizedBox(width: 12),
Expanded(
child: Text(
_scanStatus,
style: TextStyle(color: Colors.blue[800]),
),
),
],
),
),
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),
border: Border.all(color: Colors.green.withOpacity(0.3)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.check_circle, color: Colors.green, size: 20),
SizedBox(width: 8),
Text(
'Card Scan Results',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
],
),
SizedBox(height: 16),
_buildResultRow('Card Number',
ItcSunmiCardReaderService.formatCardNumber(_scanResult!.cardNumber)),
_buildResultRow('Card Type', _scanResult!.cardType),
if (_scanResult!.expiryDate.isNotEmpty)
_buildResultRow('Expiry Date',
ItcSunmiCardReaderService.formatExpiryDate(_scanResult!.expiryDate)),
if (_scanResult!.cardholderName.isNotEmpty)
_buildResultRow('Cardholder', _scanResult!.cardholderName),
if (_scanResult!.serviceCode.isNotEmpty)
_buildResultRow('Service Code', _scanResult!.serviceCode),
if (_scanResult!.track1.isNotEmpty)
_buildResultRow('Track 1', _scanResult!.track1, isLong: true),
if (_scanResult!.track2.isNotEmpty)
_buildResultRow('Track 2', _scanResult!.track2, isLong: true),
],
),
),
SizedBox(height: 20),
// Clear Results Button
if (_scanResult != null)
OutlinedButton(
onPressed: () {
setState(() {
_scanResult = null;
});
},
child: Text('Clear Results'),
),
],
),
),
);
}
Widget _buildResultRow(String label, String value, {bool isLong = false}) {
return Padding(
padding: EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'$label:',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.grey[600],
),
),
SizedBox(height: 2),
Text(
value.isNotEmpty ? value : 'N/A',
style: TextStyle(
fontSize: isLong ? 10 : 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
maxLines: isLong ? 2 : 1,
overflow: TextOverflow.ellipsis,
),
],
),
);
}
}
API Reference #
ItcSunmiCardReaderService #
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>
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);
Error Handling #
try {
final result = await ItcSunmiCardReaderService.startCardScan(amount: 50.0);
if (result != null) {
// Handle successful scan
}
} catch (e) {
if (e.toString().contains('CARD_SCAN_ERROR')) {
// Handle card scanning errors
print('Card scan error: $e');
} else if (e.toString().contains('INVALID_ARGUMENT')) {
// Handle invalid arguments
print('Invalid argument: $e');
} else if (e.toString().contains('SCAN_IN_PROGRESS')) {
// Handle scan already in progress
print('Scan already running: $e');
} else {
// Handle other errors
print('Unknown error: $e');
}
}
Troubleshooting #
Common 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. "EMV not initialized"
- ✅ Wait for EMV system to initialize (5-10 seconds)
- ✅ Retry scan after brief delay
4. "Card scan timeout"
- ✅ Ensure card is properly inserted/swiped/tapped
- ✅ Check card reader hardware functionality
- ✅ Verify card is not damaged
Device-Specific Notes #
- P2 Series: Full support for chip, swipe, and tap
- P2 Lite: May have limited contactless support
- V2 Series: Varies by model - test thoroughly
Performance Tips #
- Wait for initialization: Allow 3-5 seconds after app launch
- Clean card readers: Ensure hardware is clean and functional
- Proper card insertion: Follow device-specific card insertion guidelines
- Handle timeouts: Implement proper timeout and retry logic
Requirements #
- Flutter: >= 3.3.0
- Dart: >= 3.8.0
- Android: API level 21+ (Android 5.0+)
- Device: SUNMI POS terminal with card reading 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
Support & Contact #
For technical support and inquiries:
- Website: www.itconsortiumgh.com
- Email: [email protected]
- 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