momo_sms
A Flutter plugin that automatically captures Tanzanian mobile money transactions from SMS — even when the app is killed or in the background.
Supports M-PESA, Airtel Money, MIXX by YAS, and HaloPesa.
Features
- 📱 Zero-loss capture — SMS persisted in Room DB before Flutter is touched
- 🔄 3-layer delivery — live channel → WorkManager → cold-start drain
- 🔁 Deduplication — no double-processing across delivery paths
- 🌍 Swahili-native — OEM battery dialogs in Swahili (Tecno, Infinix, Xiaomi, Samsung)
- 🔌 Extensible — add custom provider profiles for local banks or SACCOs
- ✅ Tested — unit tests for all 4 MNO parsers + edge cases
Android only. iOS does not grant third-party apps access to SMS.
Installation
dependencies:
momo_sms: ^0.1.0
Then run:
flutter pub get
Android Setup
The plugin's AndroidManifest.xml declares the necessary permissions and
SmsReceiver automatically. No manual manifest edits required.
Add WorkManager and Room to your app's build.gradle:
// android/app/build.gradle
dependencies {
implementation "androidx.work:work-runtime-ktx:2.9.0"
implementation "androidx.room:room-runtime:2.6.1"
kapt "androidx.room:room-compiler:2.6.1"
}
Kotlin projects: ensure
apply plugin: 'kotlin-kapt'is at the top of the samebuild.gradlefile.
Usage
1. Initialize once in main()
import 'package:momo_sms/momo_sms.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await MomoSms.initialize(
onTransaction: (record) {
// record.provider → "M-PESA" | "AirtelMoney" | "MIXX BY YAS" | "HaloPesa"
// record.type → TransactionType.incoming | outgoing | unknown
// record.amount → double (TZS)
// record.balance → double? (post-transaction balance)
// record.reference → String? (provider transaction ID)
// record.receivedAt → DateTime (from SMS PDU, not wall-clock)
myDatabase.save(record);
},
);
runApp(const MyApp());
}
2. Request permissions and drain missed SMS on cold start
@override
void initState() {
super.initState();
_init();
}
Future<void> _init() async {
// Requests READ_SMS + battery optimization exemption + OEM guidance dialogs.
// Safe to call on every app open — skips steps already completed.
await SmsPermissionService.setup(context);
// Pulls any SMS that arrived while the app was dead or in the background.
await MomoSms.drainPending();
// Optional: subscribe to a live stream instead of the onTransaction callback.
MomoSms.transactionStream.listen((record) {
setState(() => _transactions.add(record));
});
}
3. Dispose on logout or app close
@override
void dispose() {
MomoSms.dispose();
super.dispose();
}
Custom Provider Profiles
Add support for a local bank, SACCO, or any SMS-based payment provider by
passing additional profiles to initialize. Custom profiles are evaluated
before the built-in MNO profiles, so they can also override existing ones.
final nmbProfile = TzProviderProfile(
name: 'NMB Bank',
senderPattern: RegExp(r'NMB', caseSensitive: false),
amountPattern: RegExp(r'TZS\s?([0-9,]+(?:\.[0-9]{1,2})?)'),
balancePattern: RegExp(r'Balance[:\s]*([0-9,]+)'),
referencePattern: RegExp(r'Ref[:\s]*([A-Z0-9]+)'),
typeResolver: (body) => body.contains('Credit')
? TransactionType.incoming
: TransactionType.outgoing,
);
await MomoSms.initialize(
onTransaction: (record) => myDatabase.save(record),
additionalProfiles: [nmbProfile],
);
See TzProviderProfile for the full list of configurable fields.
Supported Providers
| Provider | Network | SMS Language | Sender pattern |
|---|---|---|---|
| M-PESA | Vodacom | Swahili | M-PESA / MPESA |
| Airtel Money | Airtel | Swahili + English | AirtelMoney |
| MIXX by YAS | Yas/Tigo | Swahili | MIXX / Tigo / YAS |
| HaloPesa | Halotel | Swahili | HaloPesa / Halotel |
Architecture
SMS Arrives (app alive or killed)
│
▼
[SmsReceiver.kt] ← OS-guaranteed wake via manifest BroadcastReceiver
├─ Room.insert() ← atomic persist, no Flutter dependency
├─ tryLiveDelivery() ← MethodChannel if Flutter engine alive
│ └─ on success: markLiveDelivered()
└─ scheduleWorker(KEEP) ← WorkManager fallback
[SmsProcessingWorker.kt] ← fires if engine was not alive
├─ getUndelivered() ← skips liveDelivered + processed rows
├─ deliver() with await ← suspends until Flutter Result callback
├─ markProcessed() ← only after Flutter confirms
└─ max 3 retries ← gives up, defers to drain
App Opens (cold start)
├─ MomoSms.initialize()
├─ SmsPermissionService.setup() ← permissions + OEM guidance
└─ MomoSms.drainPending() ← pulls all unprocessed from Room
└─ ack per item → Kotlin markProcessed()
Running Tests
flutter test test/transaction_parser_test.dart
Contributing
Contributions are welcome — especially additional provider profiles for Tanzanian banks and SACCOs. Please open an issue before submitting a large PR.
- Fork the repo
- Create a branch:
git checkout -b feat/crdb-profile - Add your profile to
lib/src/profiles/tz_mno_profiles.dart - Add a corresponding test in
test/transaction_parser_test.dart - Open a pull request
License
MIT © 2025 tfkcodes
Libraries
- momo_sms
- momo_sms — Mobile Money SMS Transaction Tracker
- momo_sms_method_channel
- momo_sms_platform_interface