fundstrack_lite
A Flutter plugin for parsing and extracting banking transaction details from SMS inbox on Android devices.
- Detects and parses both traditional bank and UPI SMS notifications.
- Extracts: Amount, Account Number, Date, Time, Transaction Type (debit/credit), and Sender.
- ✅ Background-compatible with WorkManager support for silent syncing.
⚠️ Android only: iOS is not supported due to platform restrictions.
Features
- Reads Android SMS inbox for debit/credit transaction messages
- Extracts structured data:
- Amount
- Account (last 4/6 digits)
- Sender (e.g. HDFCBK, AXISBK, etc.)
- Date, Time
- Transaction Type:
debitorcredit - Original message body
- Built-in background support via
WorkManager - Designed for integration with Flutter UI and APIs
Installation
Add to your pubspec.yaml:
dependencies:
fundstrack_lite: ^0.0.8
permission_handler: ^11.3.0
Android Setup
- Add Permissions in AndroidManifest.xml Place the following outside the
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
2. Request Permissions at Runtime
import 'package:permission_handler/permission_handler.dart';
Future<void> requestSmsPermission() async {
await Permission.sms.request();
await Permission.phone.request();
}
Background Sync via WorkManager
You can also use the direct getter:
import 'package:fundstrack_lite/fundstrack_lite.dart';
final smsList = await FundsTrackLite.getBankSms([]); // returns List<Map<String, dynamic>>
This will enqueue a WorkManager job on the native side (Android only) to read SMS and push to your backend.
Parsing SMS Directly (Foreground)
You can also use the direct getter:
import 'package:fundstrack_lite/fundstrack_lite.dart';
final smsList = await FundsTrackLite.getBankSms([]); // returns List<Map<String, dynamic>>
Each entry includes:
| Key | Description |
|---|---|
| amount | Parsed transaction amount |
| account | Account number (masked/partial) |
| type | "debit" or "credit" |
| sender | Bank or UPI sender ID |
| date | Parsed transaction date |
| time | Parsed transaction time |
| smsTimestamp | SMS received time (epoch) |
| body | Full SMS text |
Android Plugin Implementation (Kotlin)
android/src/main/kotlin/com/fundsroom/fundstracklib/FundsTrackLitePlugin.kt
package com.fundsroom.fundstracklib
import android.content.Context
import android.content.Intent
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
class FundsTrackLitePlugin : FlutterPlugin, MethodCallHandler {
private lateinit var context: Context
override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) {
context = binding.applicationContext
val channel = MethodChannel(binding.binaryMessenger, "fundstrack_lite")
channel.setMethodCallHandler(this)
}
override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"ProcessFundsTrack" -> {
val userId = call.argument<String>("userId") ?: ""
SmsBankFetcher.userId = userId
// ✅ Launch background processor (WorkManager internally or fallback)
val intent = Intent(context, SmsProcessingService::class.java)
intent.putExtra("userId", userId)
context.startForegroundService(intent)
result.success("FundsTrack started for user $userId")
}
else -> {
result.notImplemented()
}
}
}
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
// No cleanup needed
}
}
Example Flutter App (main.dart)
import 'package:flutter/material.dart';
import 'package:fundstrack_lite/fundstrack_lite.dart';
import 'package:permission_handler/permission_handler.dart';
void main() {
runApp(const FundsTrackLiteDemoApp());
}
class FundsTrackLiteDemoApp extends StatelessWidget {
const FundsTrackLiteDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'fundstrack_lite Demo',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.teal),
useMaterial3: true,
),
home: const BankSmsScreen(),
);
}
}
class BankSmsScreen extends StatefulWidget {
const BankSmsScreen({super.key});
@override
State<BankSmsScreen> createState() => _BankSmsScreenState();
}
class _BankSmsScreenState extends State<BankSmsScreen> {
List<Map<String, dynamic>>? _smsList;
String? _error;
bool _loading = false;
@override
void initState() {
super.initState();
_loadSms();
}
Future<void> _loadSms() async {
setState(() {
_loading = true;
_error = null;
});
try {
final sms = await Permission.sms.request();
final phone = await Permission.phone.request();
if (!sms.isGranted || !phone.isGranted) {
setState(() {
_error = 'SMS & phone permissions required.';
_loading = false;
});
return;
}
final smsList = await FundsTrackLite.getBankSms([]);
setState(() {
_smsList = smsList;
_loading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_loading = false;
});
}
}
@override
Widget build(BuildContext context) {
Widget body;
if (_loading) {
body = const Center(child: CircularProgressIndicator());
} else if (_error != null) {
body = Center(child: Text('Error: $_error'));
} else if (_smsList == null || _smsList!.isEmpty) {
body = const Center(child: Text('No debit/credit SMS found.'));
} else {
body = _smsListView(_smsList!);
}
return Scaffold(
appBar: AppBar(
title: const Text('Bank SMS Transactions'),
actions: [
IconButton(
icon: const Icon(Icons.refresh),
onPressed: _loadSms,
tooltip: "Refresh",
),
],
),
body: body,
);
}
Widget _smsListView(List<Map<String, dynamic>> smsList) {
return ListView.separated(
itemCount: smsList.length,
separatorBuilder: (_, __) => const Divider(),
itemBuilder: (context, index) {
final sms = smsList[index];
final DateTime smsTime = DateTime.fromMillisecondsSinceEpoch(
int.tryParse(sms['smsTimestamp']?.toString() ?? '0') ?? 0,
);
final dateStr = sms['date']?.isNotEmpty == true
? sms['date']
: "${smsTime.day.toString().padLeft(2, '0')}/${smsTime.month.toString().padLeft(2, '0')}/${smsTime.year}";
final timeStr = sms['time']?.isNotEmpty == true
? sms['time']
: "${smsTime.hour.toString().padLeft(2, '0')}:${smsTime.minute.toString().padLeft(2, '0')}";
return Card(
margin: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
elevation: 2,
child: Padding(
padding: const EdgeInsets.all(14.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Amount: ${sms['amount'] ?? '--'}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
Text("Account Number: ${sms['account'] ?? '--'}"),
Text("Sender: ${sms['sender'] ?? '--'}"),
Text("Type: ${sms['type'] ?? '--'}",
style: TextStyle(
color: (sms['type'] == 'debit')
? Colors.red
: (sms['type'] == 'credit')
? Colors.green
: Colors.black,
fontWeight: FontWeight.bold,
)),
Text("Date: $dateStr"),
Text("Time: $timeStr"),
// Text("Body: ${sms['body']}"),
],
),
),
);
},
);
}
}
API Integration
You can push SMS data to a backend API:
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> sendSmsToServer(Map<String, dynamic> sms) async {
await http.post(
Uri.parse('https://your-api-endpoint.com/sms'),
headers: {'Content-Type': 'application/json'},
body: jsonEncode(sms),
);
}
NDK Compatibility Fix (Optional)
If you see this error:
NDK version mismatch
plugin fundstrack_lite requires NDK 27.0.12077973
Add this to your android/app/build.gradle.kts:
android {
ndkVersion = "27.0.12077973"
}
Limitations
- ❌ iOS not supported (OS does not allow SMS inbox access)
- Works only if
READ_SMSpermission is granted - Parsing may fail for non-standard bank formats
WorkManagerbackground jobs are Android-only
Notes
- The plugin registers itself automatically via onAttachedToEngine() using Flutter v2 embedding.
- You must declare this in your pubspec.yaml:
plugin:
platforms:
android:
package: com.fundsroom.fundstracklib
pluginClass: FundsTrackLitePlugin
Contributions
We welcome contributions — help improve regex accuracy for different bank formats or submit issues/PRs for real-world data enhancements.