
Streamlines Vodacom Mozambique M‑Pesa C2B checkout by handling authentication, RSA encryption, HTTP orchestration, composable UI flows, input validation, and reactive transaction results with localized messaging.
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.
Gradle (Android)
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.nand-industries:mpesa-multiplatform-sdk:<version>")
}SwiftPM (iOS)
Check the full installation guide here.
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)
import UIKit
import SwiftUI
import MpesaMultiplatformSdk
struct ContentView: View {
var body: some View {
VStack {
MpesaCheckoutView()
}
}
}
struct MpesaCheckoutView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainKt.MainViewController(
businessName: "Sample Shop",
businessLogoUrl: "yourbusinesslogo.xyz",
transactionReference: "T123456",
thirdPartyReference: "12345",
defaultAmount: "499.50",
defaultPhoneNumber: "841234567",
onDismissInputTransactionDetailsStep: {}
)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}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/ECB/PKCS1Padding before being sent as bearer
tokens. You can override via RsaEncryptHelper per‑platform.api.sandbox.vm.co.mz) and production (
api.vm.co.mz) via the isProduction flag.Always store API keys securely (encrypted at rest or fetched from your backend) and inject them at runtime. Never commit real keys.
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:
local.properties
iosApp/Configuration/Config.xcconfig
MPESA_PRODUCTION_API_KEY=replace
MPESA_DEVELOPMENT_API_KEY=replace
MPESA_PUBLIC_KEY=replaceRun:
./gradlew :androidApp:installDebug
iosApp in Xcode and run the iosApp scheme./gradlew :sdk:check
feature/<short-description>, fix/<short-description>,
chore/<short-description>, refactor/<short-description>
or security/<short-description>from main.Res.string.*).sdk/src/commonTest when changing business logic. Run
./gradlew :sdk:check before pushing.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.
Gradle (Android)
repositories {
mavenCentral()
}
dependencies {
implementation("io.github.nand-industries:mpesa-multiplatform-sdk:<version>")
}SwiftPM (iOS)
Check the full installation guide here.
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)
import UIKit
import SwiftUI
import MpesaMultiplatformSdk
struct ContentView: View {
var body: some View {
VStack {
MpesaCheckoutView()
}
}
}
struct MpesaCheckoutView: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> UIViewController {
MainKt.MainViewController(
businessName: "Sample Shop",
businessLogoUrl: "yourbusinesslogo.xyz",
transactionReference: "T123456",
thirdPartyReference: "12345",
defaultAmount: "499.50",
defaultPhoneNumber: "841234567",
onDismissInputTransactionDetailsStep: {}
)
}
func updateUIViewController(_ uiViewController: UIViewController, context: Context) {
}
}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/ECB/PKCS1Padding before being sent as bearer
tokens. You can override via RsaEncryptHelper per‑platform.api.sandbox.vm.co.mz) and production (
api.vm.co.mz) via the isProduction flag.Always store API keys securely (encrypted at rest or fetched from your backend) and inject them at runtime. Never commit real keys.
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:
local.properties
iosApp/Configuration/Config.xcconfig
MPESA_PRODUCTION_API_KEY=replace
MPESA_DEVELOPMENT_API_KEY=replace
MPESA_PUBLIC_KEY=replaceRun:
./gradlew :androidApp:installDebug
iosApp in Xcode and run the iosApp scheme./gradlew :sdk:check
feature/<short-description>, fix/<short-description>,
chore/<short-description>, refactor/<short-description>
or security/<short-description>from main.Res.string.*).sdk/src/commonTest when changing business logic. Run
./gradlew :sdk:check before pushing.