Unofficial, open‑source Compose Multiplatform SDK that streamlines checkout (C2B) integration with the Vodacom Mozambique M‑Pesa API for Android and iOS (with room to extend to Web, Desktop and JVM Systems).
It wraps authentication, HTTP orchestration, UI flows, and reactive transaction reporting behind a single API so Android and iOS apps share the same logic and presentation layer. This project also serves as an experiment on Compose Multiplatform capabilities for building libraries targeting native mobile platforms.
If you plan to go live, read the official Vodacom M-Pesa API docs carefully and validate your flows in sandbox and production. Also, make sure to take a look at M-Pesa Requirements Checker.
In order to integrate this SDK to your mobile application you need to do the following steps:
You need an API Key and a Public Key to initialize the SDK and connect to the M‑Pesa API. Create an account and generate credentials in the Vodacom Developer Portal. Make sure you understand sandbox vs production behavior before shipping.
Note: Distribution is being finalized. The snippets below show how it will look once published. Until then, use the included sample apps or consume the SDK via source.
Gradle (Android)
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.nand-industries:mpesa-multiplatform-sdk:<version>")
}SwiftPM (iOS)
.package(url: "https://github.com/nand-industries/mpesa-multiplatform-sdk.git", from: "<version>")Call MpesaMultiplatformSdkInitializer once before presenting any checkout UI, ideally during
app startup or DI graph creation.
Android
import io.github.nandindustries.sdk.config.MpesaMultiplatformSdkInitializer
MpesaMultiplatformSdkInitializer.init(
productionApiKey = BuildConfig.MPESA_PRODUCTION_API_KEY,
developmentApiKey = BuildConfig.MPESA_DEVELOPMENT_API_KEY,
publicKey = BuildConfig.MPESA_PUBLIC_KEY,
serviceProviderCode = "171717",
isProduction = BuildConfig.DEBUG.not(),
// Optional: rsaEncryptHelper = YourAndroidRsaEncryptHelper()
)iOS
import MpesaMultiplatformSdk
MpesaMultiplatformSdkInitializer().doInit(
productionApiKey: productionApiKey,
developmentApiKey: developmentApiKey,
publicKey: publicApiKey,
isProduction: false,
serviceProviderCode: "171717",
rsaEncryptHelper: /* required on iOS — provide your own */
)On Android, providing a custom RSA helper is optional. On iOS, you must implement and pass an
RsaEncryptHelper. See the iOS demo app for a minimal implementation.
The SDK renders an input screen followed by a processing screen and wraps the C2B API under the hood. You trigger it from your host app.
Android (Compose)
import io.github.nandindustries.sdk.ui.navigation.MpesaMultiplatformNavigationGraph
MpesaMultiplatformNavigationGraph(
businessName = "Sample Shop",
businessLogoUrl = "yourbusinesslogo.xyz",
transactionReference = "T123456",
thirdPartyReference = "12345",
defaultAmount = "499.50", // Optional — if omitted (not editable), user must enter
defaultPhoneNumber = "841234567", // Optional — if omitted (editable), user must enter
onDismissInputTransactionDetailsStep = { /* Close sheet */ },
)iOS (SwiftUI/UIKit host)
let controller = MainViewController(
businessName: "Sample Shop",
businessLogoUrl: "yourbusinesslogo.xyz",
transactionReference: "T123456",
thirdPartyReference: "12345",
defaultAmount: "499.50",
defaultPhoneNumber: "841234567"
)
present(controller, animated: true)
MainViewControllerdelegates rendering to the shared Compose navigation graph, so the UI matches Android.
Subscribe to the shared stream to be notified when the user cancels the flow or when M‑Pesa
responds. CustomerToBusinessUseCase maps M‑Pesa response codes to strongly typed outcomes with
localized messaging.
Android
val transactionStream = MpesaMultiplatformSdk.transactionCompletionStream
lifecycleScope.launch {
transactionStream.onTransactionCompletionResult().collect { result ->
when (result) {
is TransactionCompletionResult.C2BTransactionCompleted -> {
if (result.result is CustomerToBusinessUseCase.Result.SuccessfulTransaction) {
// Handle success
} else {
// Handle failure
}
}
is TransactionCompletionResult.C2BTransactionCancelledBeforeStarted -> {
// Handle cancellation
}
}
}
}iOS
TransactionCompletionObserverKt.observeTransactionCompletion { result in
if result is TransactionCompletionResultC2BTransactionCancelledBeforeStarted {
// Handle cancellation
} else if let completed = result as? TransactionCompletionResultC2BTransactionCompleted {
if TransactionCompletionResultExtensionsKt.isSuccessfulC2BTransaction(result: completed) {
// Handle success
} else {
// Handle failure
}
}
}- RSA encryption: API keys are encrypted with
RSA/ECB/PKCS1Paddingbefore being sent as bearer tokens. You can override viaRsaEncryptHelperper‑platform. - Env isolation: The SDK switches between sandbox (
api.sandbox.vm.co.mz) and production (api.vm.co.mz) via theisProductionflag. - Input hardening: Phone numbers are constrained to Vodacom prefixes (84/85) and exactly nine digits. Amounts are coerced to configured min/max before submission.
- Localized responses: Response codes map to user‑friendly titles/subtitles/descriptions to avoid leaking raw API codes.
Always store API keys securely (encrypted at rest or fetched from your backend) and inject them at runtime. Never commit real keys.
- Kotlin 1.9+ with Compose Multiplatform plugin (toolchain 21 configured).
- Android Studio Giraffe+ or IntelliJ IDEA with KMP support.
- Xcode 15+ for iOS builds.
mpesa-multiplatform/
├── androidApp/ # Android sample (Compose)
├── iosApp/ # iOS sample project (SwiftUi)
├── sdk/ # Kotlin Multiplatform library module
│ ├── src/commonMain/ # Shared business logic, UI, resources
│ ├── src/androidMain/ # Android-specific HTTP & crypto wiring
│ └── src/iosMain/ # iOS-specific HTTP & crypto wiring
└── build.gradle.kts
Add these lines to local secrets and replace with your values:
- Android:
local.properties - iOS:
iosApp/Configuration/Config.xcconfig
MPESA_PRODUCTION_API_KEY=replace
MPESA_DEVELOPMENT_API_KEY=replace
MPESA_PUBLIC_KEY=replaceRun:
- Android:
./gradlew :androidApp:installDebug - iOS: open
iosAppin Xcode and run theiosAppscheme - Shared tests:
./gradlew :sdk:check
- Check for existing Issues: Before proposing a new idea or reporting a bug, search the project’s issue tracker to ensure no duplicate or similar issues already exist. Use relevant keywords and filters to review open and closed issues.
- Propose: Open an issue describing your idea or bug before starting work.
- Branching: Use
feature/<short-description>,fix/<short-description>,chore/<short-description>,refactor/<short-description>orsecurity/<short-description>frommain. - Code Style: Follow Kotlin official style and idiomatic Compose. Use resource strings for
user‑facing text (
Res.string.*). - Testing: Put unit tests under
sdk/src/commonTestwhen changing business logic. Run./gradlew :sdk:checkbefore pushing. - Security: Never commit real credentials. Use env vars or local secrets.
- Verify Samples: Ensure both sample apps build and run.
- PRs: Include a clear summary, screenshots for UI changes, and note any breaking API changes. Ensure CI passes before requesting review.

