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.

pub.dev License: MIT Platform


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 same build.gradle file.


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.

  1. Fork the repo
  2. Create a branch: git checkout -b feat/crdb-profile
  3. Add your profile to lib/src/profiles/tz_mno_profiles.dart
  4. Add a corresponding test in test/transaction_parser_test.dart
  5. 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