Flutterwave Integration With Compose


Flutterwave remains one of the most reliable gateway for businesses across Africa and beyond. However, most existing resources focus on traditional XML-based UI and deprecated onActivityResult()
methods, leaving a gap for developers building apps with Jetpack Compose. In this tutorial, we’ll walk through the step-by-step process of integrating Flutterwave into a Compose project—from setup to handling transactions—so you can start accepting payments seamlessly in your app.
Project Requirements
It is important to note that this tutorial will utilise Flutterwave's default Drop In UI thus, apart from the knowing the basis of Android Native Development, the only other requirements are
A Flutterwave account with encryption and public keys (for testing purposes only, feel free to use the ones in this tutorial).
Gradle dependencies.
Manifest permission.
Setting Up Your Project
Now create a new Android project with compose and use Kotlin DSL for your gradle. After it builds, add the following dependencies to the app-level gradle like in the example below:
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("com.google.code.gson:gson:2.11.0")
implementation("com.github.flutterwave.rave-android:rave_android:2.2.1")
implementation("org.parceler:parceler-api:1.1.13")
Side bar: Flutterwave requires AppCompactActivity thus we need to add it to our gradle (if not already present) and tweak our themes.xml to avoid crashes. Simply change the parent from
parent="android:Theme.Material.Light.NoActionBar"
toparent="Theme.AppCompat.Light.NoActionBar"
In our settings.gradle.kts, in the dependencyResolutionManagement
section, replace :
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
With:
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
Now in the repositories
section just below, add the following:
maven { url = uri("https://jitpack.io") }
Your dependencyResolutionManagement
in settings.gradle.kts should look like this:
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
repositories {
maven { url = uri("https://jitpack.io") }
mavenCentral()
google()
}
}
Sync the changes.
Add the following permission to our mainifest just before the application tag, and we're good to go:
<uses-permission android:name="android.permission.INTERNET"/>
Overview
The basic idea is represented by the diagram below. A user initiates the payment by pressing a button [1] which sends a request to a secure backend server. The response contains public and encryption keys of our Flutterwave account as well as other required parameters to configure a payment request to Flutterwave [2] . The configurations are then packaged as an intent [3] launched using rememberLauncherForActivityResult
to start the Flutterwave default Drop UI [4] . Flutterwave then processes the transaction and sends a response back [5] .
Setting up UI
For the UI, a simple TextField
and a Button
should do. The TextField
is wired to accept only decimals with two decimal places
import android.app.Activity
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.flutterwavecompseintegration.ui.theme.FlutterwaveCompseIntegrationTheme
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
FlutterwaveCompseIntegrationTheme {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ScreenContent(this,innerPadding)
}
}
}
}
}
@Composable
fun ScreenContent(activity:Activity, innerPadding: PaddingValues) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(innerPadding),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
var amount by remember { mutableStateOf("") }
TextField(
value = amount,
onValueChange = { input ->
// Regex: optional digits, optional decimal with up to 2 digits
val regex = Regex("^(0|[1-9]\\d*)(\\.\\d{0,2})?$")
if (input.isEmpty()) {
amount = ""
} else if (regex.matches(input)) {
amount = input
}
},
placeholder = { Text("Enter Amount") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Decimal,
imeAction = ImeAction.Done
),
modifier = Modifier
.fillMaxWidth(0.8f) // 80% of screen width
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* Handle click */ },
modifier = Modifier.fillMaxWidth(0.5f) // 50% of screen width
) {
Text("Pay")
}
}
}
}
@Preview(showBackground = true)
@Composable
fun ScreenContentPreview() {
FlutterwaveCompseIntegrationTheme {
ScreenContent(PaddingValues())
}
}
Integrating Flutterwave
Let's create a file in our project directory to hold our Flutterwave utilities called flutterwave_utils
(or whatever that works for you).
Ideally, the encryption and public key should come from a secure backend; it should never be hardcoded in any file as I am about to do here. In addition, if you do get it from a backend, do not log it nor cache it to a room Db or shared preferences.
Once a user initiates a payment, it should send a request to your backend which then sends a response containing the keys, narration and other useful information like additional Metadata. A class that shows what a response could look like is shown below:
import com.google.gson.annotations.SerializedName
data class FlutterwavePaymentRequest(
@SerializedName("email")
val email: String? = null,
@SerializedName("country")
val country: String? = null,
@SerializedName("currency")
val currency: String? = null,
@SerializedName("amount")
val amount: Double? = null,
@SerializedName("first_name")
val firstName: String? = null,
@SerializedName("last_name")
val lastName: String? = null,
@SerializedName("narration")
val narration: String? = null,
@SerializedName("reference")
val reference: String? = null,
@SerializedName("public_key")
val publicKey: String? = null,
@SerializedName("encryption_key")
val encryptionKey: String? = null,
@SerializedName("is_staging")
val isStaging: Boolean? = null,
@SerializedName("metadata") val metadata: Map<String, Any>? = null
)
For this tutorial, an hardcoded instance would be used Public and Encryption keys provided by GideonJon. Thanks
in our Button's onClick
onClick = {
val amountToPay = amount.toDoubleOrNull() ?: return@Button
val fee = 209.09
val paymentRequest = FlutterwavePaymentRequest(
email = "tester@email.com",
country = "NG",
currency = "NGN",
amount = amountToPay,
firstName = "first name",
lastName = "last name",
narration = "payment for item",
reference = "txt_ref123456",
isStaging = true,
publicKey = "FLWPUBK_TEST-06fe0b1c5d0e3af287d0ec5c99dec6f0-X",
encryptionKey = "FLWSECK_TESTf5dca3a1293a",
metadata = mapOf(
"paymentUserId" to "user_id_1234",
"fee" to fee,
"total" to amountToPay + fee,
"totalInKobo" to (amountToPay + fee) * 100
)
)
}
Using the hardcoded FlutterwavePaymentRequest
instance, we will create an instance of RavePayInitializer
from Flutterwave rather than start the RaveUiManager
(Flutterwave's default Drop In UI activity). One of the cool things about Flutterwave is its extensive payment options. From card and USSD to Mpessa, UgMobileMoney and many more so here's where we can customise it:
fun createRavePayParams(
paymentRequest: FlutterwavePaymentRequest,
acceptAccountPayments: Boolean = true,
acceptUssdPayments: Boolean = true,
acceptBankTransferPayments: Boolean = true,
acceptMpesaPayments: Boolean = false,
acceptUkPayments: Boolean = false,
acceptUgMobileMoneyPayments: Boolean = false,
acceptZmMobileMoneyPayments: Boolean = false,
acceptRwfMobileMoneyPayments: Boolean = false,
acceptAchPayments: Boolean = false,
acceptGHMobileMoneyPayments: Boolean = false,
acceptSaBankPayments: Boolean = false,
acceptFrancMobileMoneyPayments: Boolean = false,
theme: Int = com.flutterwave.raveandroid.R.style.DefaultTheme
): RavePayInitializer {
val paymentMethods = arrayListOf(RaveConstants.PAYMENT_TYPE_CARD)
if (acceptAccountPayments) {
paymentMethods.add(RaveConstants.PAYMENT_TYPE_ACCOUNT)
}
if (acceptUssdPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_USSD)
}
if (acceptBankTransferPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_BANK_TRANSFER)
}
if (acceptMpesaPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_MPESA)
}
if (acceptUkPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_UK)
}
if (acceptUgMobileMoneyPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_UG_MOBILE_MONEY)
}
if (acceptZmMobileMoneyPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_ZM_MOBILE_MONEY)
}
if (acceptRwfMobileMoneyPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_RW_MOBILE_MONEY)
}
if (acceptAchPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_ACH)
}
if (acceptGHMobileMoneyPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_GH_MOBILE_MONEY)
}
if (acceptSaBankPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_SA_BANK_ACCOUNT)
}
if (acceptFrancMobileMoneyPayments){
paymentMethods.add(RaveConstants.PAYMENT_TYPE_FRANCO_MOBILE_MONEY)
}
with(paymentRequest) {
return RavePayInitializer(
email,
amount ?: 0.0,
publicKey,
encryptionKey,
reference,
narration,
currency,
country,
"NG",
firstName,
lastName,
theme,
"",
true,
true,
true,
false,
0,
0,
isStaging ?: true,
stringifyMeta(metadata?.map { Meta(it.key, it.value.toString()) }),
"",
null,
false,
true,
true,
paymentMethods
)
}
}
The createRavePayParams()
function takes the request, different payment methods(itemised in the function arguments) as well as theme. We start by creating a list of preferred payment methods then using the request instance, fill in the remaining values.
Handle the response
From official resources, a typical response for every transaction looks like this. The response has been mapped to a data class or Plain Old Java Object(POJO) named FlutterwaveTransactionResponse
. This lets us view transaction status, perform transaction verification with your reference or access the meta and to display the response message to users. This would be added to flutterwave_utils.kt.
The response is like this:
data class FlutterwaveTransactionResponse(
@SerializedName("status")
val status: String? = null,
@SerializedName("message")
val message: String? = null,
@SerializedName("data")
val data: FlutterwaveTransactionData? = null
)
data class FlutterwaveTransactionData(
@SerializedName("id") val id: Int? = null,
@SerializedName("txRef") val txRef: String? = null,
@SerializedName("flwRef") val flwRef: String? = null,
@SerializedName("orderRef") val orderRef: String? = null,
@SerializedName("redirectUrl") val redirectUrl: String? = null,
@SerializedName("device_fingerprint") val deviceFingerprint: String? = null,
@SerializedName("cycle") val cycle: String? = null,
@SerializedName("amount") val amount: Double? = null,
@SerializedName("charged_amount") val chargedAmount: Double? = null,
@SerializedName("appfee") val appFee: Double? = null,
@SerializedName("merchantfee") val merchantFee: Double? = null,
@SerializedName("merchantbearsfee") val merchantBearsFee: Int? = null,
@SerializedName("chargeResponseCode") val chargeResponseCode: String? = null,
@SerializedName("raveRef") val raveRef: String? = null,
@SerializedName("chargeResponseMessage") val chargeResponseMessage: String? = null,
@SerializedName("currency") val currency: String? = null,
@SerializedName("IP") val ip: String? = null,
@SerializedName("narration") val narration: String? = null,
@SerializedName("status") val status: String? = null,
@SerializedName("modalauditid") val modalAuditId: String? = null,
@SerializedName("chargeRequestData") val chargeRequestData: String? = null,
@SerializedName("chargeResponseData") val chargeResponseData: String? = null,
@SerializedName("retry_attempt") val retryAttempt: String? = null,
@SerializedName("getpaidBatchId") val getPaidBatchId: String? = null,
@SerializedName("createdAt") val createdAt: String? = null,
@SerializedName("updatedAt") val updatedAt: String? = null,
@SerializedName("deletedAt") val deletedAt: String? = null,
@SerializedName("customerId") val customerId: Int? = null,
@SerializedName("AccountId") val accountId: Int? = null,
@SerializedName("customer.id") val customerIdAlt: Int? = null,
@SerializedName("customer.phone") val customerPhone: String? = null,
@SerializedName("customer.fullName") val customerFullName: String? = null,
@SerializedName("customer.customertoken") val customerToken: String? = null,
@SerializedName("customer.email") val customerEmail: String? = null,
@SerializedName("customer.createdAt") val customerCreatedAt: String? = null,
@SerializedName("customer.updatedAt") val customerUpdatedAt: String? = null,
@SerializedName("customer.deletedAt") val customerDeletedAt: String? = null,
@SerializedName("customer.AccountId") val customerAccountId: Int? = null,
@SerializedName("meta") val meta: List<Any>? = null,
@SerializedName("flwMeta") val flwMeta: FlwMeta? = null
)
data class FlwMeta(
@SerializedName("chargeResponse") val chargeResponse: String? = null,
@SerializedName("chargeResponseMessage") val chargeResponseMessage: String? = null
)
Launch the intent
Now create a rememberLauncherForActivityResult
in MainActivity
to handle results without using the deprecated onActivityResult()
.
// Launcher to start RavePayActivity and get result
val paymentLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
val response = result.data?.getStringExtra("response")
val gson = Gson()
val jsonType = object : TypeToken<FlutterwaveTransactionResponse>() {}.type
val transactionResponse = if (!response.isNullOrEmpty()) gson.fromJson<FlutterwaveTransactionResponse>(
response,
jsonType
) else null
when (result.resultCode) {
RavePayActivity.RESULT_SUCCESS -> {
if (transactionResponse != null) {
Toast.makeText(
activity,
transactionResponse.data?.chargeResponseMessage ?: "Success",
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(activity, "SUCCESS", Toast.LENGTH_SHORT).show()
}
}
RavePayActivity.RESULT_ERROR -> {
if (transactionResponse != null) {
Toast.makeText(
activity,
transactionResponse.data?.chargeResponseMessage ?: "Error",
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(activity, "ERROR", Toast.LENGTH_SHORT).show()
}
}
RavePayActivity.RESULT_CANCELLED -> {
Toast.makeText(activity, "CANCELLED", Toast.LENGTH_SHORT).show()
}
}
}
Putting it all together now, the Button
click can now launch the intent
val intent = Intent(activity, RavePayActivity::class.java)
intent.putExtra(
RaveConstants.RAVE_PARAMS,
Parcels.wrap(createRavePayParams(paymentRequest))
)
paymentLauncher.launch(intent)
Important
Please please please do NOT test the card payment with a REAL CARD unless you are ready to lodge a reversal complaint at the bank.
Conclusion
I really hope you find this useful. Do let me know in the comment section if you would like a tutorial on doing this without the default UI, or help customise the style.
For more information, check out the official documentation here The project can be viewed on GitHub here
Subscribe to my newsletter
Read articles from Abdullahi Musa directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
